← Devlog
Rex's Ranch

Using a Finite State Machine for Farmer Behaviours

· Tyrannoseanus
Using a Finite State Machine for Farmer Behaviours

Early on in development I swapped out my finite state machine implementation with a behaviour tree for the farmer, but I think there’s room for both.

Don’t get me wrong — the behaviour tree is fantastic and it’s made the farmer behaviour so much simpler to implement. But by completely getting rid of the concept of the finite state machine, it’s made implementing the individual behaviours a lot harder than it needed to be if I had used the best of both.

Anyway here’s the previous version of the HarvestCropTask in the behaviour tree:

class_name HarvestCropTask extends Task

var can_harvest_crop: bool = true
var crop_harvested: bool = false
var can_place_crop: bool = true
var crop_placed: bool = false
var target_area: Crop

func _init(area: Crop):
	target_area = area

func execute(actor: FarmActor, delta: float) -> int:
	SignalBus.farmer_harvesting.emit()
	if !crop_harvested:
		actor.move_to(target_area.global_position)
		if !actor.navigation_finished():
			return BTNode.BTStatus.RUNNING
	if can_harvest_crop && !crop_harvested:
		can_harvest_crop = false
		crop_harvested = actor.harvest(target_area)
		GlobalTimer.create_timer(.5).timeout.connect(reset_harvest_crop)
	if can_place_crop && crop_harvested:
		actor.carrying = true
		var carry_crop := target_area.harvest()
		if carry_crop == null:
			return BTNode.BTStatus.RUNNING
		actor.add_child(carry_crop)
		carry_crop.position = Vector2(0, -10)
		var shipping_box := actor.get_tree().get_first_node_in_group("shipping_box")
		actor.move_to(shipping_box.global_position)
		if !actor.navigation_finished():
			return BTNode.BTStatus.RUNNING
		crop_placed = actor.ship(shipping_box, carry_crop.data)
		var tween := carry_crop.create_tween()
		tween.tween_property(carry_crop, "global_scale", Vector2(0.1, 0.1), 0.3)
		can_place_crop = false
		GlobalTimer.create_timer(0.8).timeout.connect(reset_place_crop)
		if crop_placed:
			actor.carrying = false
			carry_crop.queue_free()
			return BTNode.BTStatus.SUCCESS
	return BTNode.BTStatus.RUNNING

For the sake of my reputation I probably should have cleaned this up a bit first but that’s ok — it’s messy, it’s convoluted, and it is hard to extend. This is partway through adding regrowable crops to the game and struggling to get that implemented. This will make it easier to prove the point that a finite state machine can make all of those problems go away with a little bit of refactoring.

So the post picture spoils the outcome a little:

class_name HarvestCropTask extends Task

enum HarvestState { WAIT, MOVE_TO_CROP, HARVEST, PICKUP, MOVE_TO_BOX, SHIP, DONE }

var state: int = HarvestState.MOVE_TO_CROP
var target_crop: Crop
var shipping_box: ShippingBox
var carried_crop: Crop

func _init(crop: Crop):
	target_crop = crop
	shipping_box = area.get_tree().get_first_node_in_group("shipping_box")

func execute(actor: FarmActor, delta: float) -> int:
	SignalBus.farmer_harvesting.emit()
	match state:
		HarvestState.MOVE_TO_CROP: 
			_move_to_crop(actor)
		HarvestState.HARVEST: 
			_harvest_crop(actor)
		HarvestState.PICKUP: 
			_pick_up_crop(actor)
		HarvestState.MOVE_TO_BOX: 
			_move_to_box(actor)
		HarvestState.SHIP: 
			_ship_crop(actor)
		HarvestState.DONE: 
			return BTNode.BTStatus.SUCCESS
		HarvestState.WAIT: 
			pass
	return BTNode.BTStatus.RUNNING

It’s longer again but that’s because it’s more explicit — the stuff that we’d need to keep in our heads is in the script instead, which is a better place for it.

This state machine is implemented much more simply than the node-based one I was using before — by storing the state within the task script itself and checking it when we execute the task as part of the behaviour tree. This makes it really easy to use in conjunction with the behaviour tree and have everything self-contained within that task node.

Anyway, regrowable crops now (nearly) work, and I’ll look at getting the other tasks migrating over to a state machine architecture where it makes sense.