Akka.NET: Delegating work to child actors

akkadotnet-logoToday I will continue to present my work on AkkaOfEmpires, in this project I implement some of the rules from the game Age Of Empires II using the actor model with Akka.NET. You can have a look at the first step here.

In this last entry I introduced the concept of behavior switching using Akka.NET while providing an implementation for a villager (unit in the game) gathering various types of resources. In this article I will continue to work on the VillagerActor and I will mostly do refactoring.

280px-Age_of_Empires_2_The_Age_of_Kings_LogoFor the moment the villager has a dedicated method for all the professions he can have when gathering resource, see the class below.

public class VillagerActor : ReceiveActor
{
    private readonly IActorRef _resourcesSupervisor;
 
    public VillagerActor(IActorRef resourcesSupervisor)
    {
        _resourcesSupervisor = resourcesSupervisor;
        Profession = Profession.Idle;
    }
 
    public Profession Profession { get; private set; }
    public Resource ResourceToRecolt { get; private set; }
 
    protected override void PreStart()
    {
        base.PreStart();
        Become(Idle);
    }
 
    private void Idle()
    {
        CommandsHandler();
    }
 
    private void Gatherer()
    {
        Profession = Profession.Gatherer;
        ResourceToRecolt = Resource.Food;
        // repeat until new order or lack of bushes
        _resourcesSupervisor.Tell(new ResourceRecolted { ResourceType = ResourceToRecolt, Quantity = 10 });
 
        CommandsHandler();
    }
 
    private void Shepherd()
    {
        Profession = Profession.Shepherd;
        ResourceToRecolt = Resource.Food;
        // repeat until new order or lack of sheeps
        _resourcesSupervisor.Tell(new ResourceRecolted { ResourceType = ResourceToRecolt, Quantity = 10 });
 
        CommandsHandler();
    }
 
    private void Hunter()
    {
        Profession = Profession.Hunter;
        ResourceToRecolt = Resource.Food;
 
        _resourcesSupervisor.Tell(new ResourceRecolted{ResourceType = ResourceToRecolt, Quantity = 10});
 
        CommandsHandler();
    }
 
    private void Farmer()
    {
        Profession = Profession.Farmer;
        ResourceToRecolt = Resource.Food;
 
        _resourcesSupervisor.Tell(new ResourceRecolted { ResourceType = ResourceToRecolt, Quantity = 10 });
 
        CommandsHandler();
    }
 
    private void Fisherman()
    {
        Profession = Profession.Fisherman;
        ResourceToRecolt = Resource.Food;
 
        _resourcesSupervisor.Tell(new ResourceRecolted { ResourceType = ResourceToRecolt, Quantity = 10 });
 
        CommandsHandler();
    }
 
    private void Lumberjack()
    {
        Profession = Profession.Lumberjack;
        ResourceToRecolt = Resource.Wood;
 
        _resourcesSupervisor.Tell(new ResourceRecolted {ResourceType = ResourceToRecolt, Quantity = 10});
        CommandsHandler();
    }
 
    private void StoneMiner()
    {
        Profession = Profession.StoneMiner;
        ResourceToRecolt = Resource.Stone;
 
        _resourcesSupervisor.Tell(new ResourceRecolted { ResourceType = ResourceToRecolt, Quantity = 10 });
        CommandsHandler();
    }
 
    private void GoldMiner()
    {
        Profession = Profession.GoldMiner;
        ResourceToRecolt = Resource.Gold;
 
        _resourcesSupervisor.Tell(new ResourceRecolted { ResourceType = ResourceToRecolt, Quantity = 10 });
        CommandsHandler();
    }
 
    private void CommandsHandler()
    {
        Receive<GatherFruits>(m => Become(Gatherer));
        Receive<ShepherdFlock>(m => Become(Shepherd));
        Receive<HuntPrey>(m => Become(Hunter));
        Receive<FarmCrops>(m => Become(Farmer));
        Receive<CatchFish>(m => Become(Fisherman));
        Receive<CutTrees>(m => Become(Lumberjack));
        Receive<MineStone>(m => Become(StoneMiner));
        Receive<MineGold>(m => Become(GoldMiner));
    }
}

As you can see the code is quite redundant and there is already a lot of methods for the class. Yet a villager can do a lot more than simply gathering resource and I fear that my class will become too big. When working with Object Oriented Programming (OOP) I try to follow the SOLID principles and I feel like I’m starting to break the Single Responsibility Principle.

My goal is to create a child actor for the VillagerActor to deal with all the resource gathering to avoid having too much logic inside this class. But first I will do some refactoring.

Refactoring phase

You may have noticed that all the commands related to the resource gathering are similar, they are associated to a type of resource and to a profession, it looks like a good opportunity to introduce an interface for all of these commands.

public interface IHarvestResourceCommand
{
    Resource ResourceToRecolt { get; }
    Profession AssociatedProfession { get; }
}
 
public abstract class HarvestFood : IHarvestResourceCommand
{
    public Resource ResourceToRecolt
    {
        get { return Resource.Food; }
    }
 
    public abstract Profession AssociatedProfession { get; }
}
 
public abstract class HarvestWood : IHarvestResourceCommand
{
    public Resource ResourceToRecolt
    {
        get { return Resource.Wood; }
    }
 
    public abstract Profession AssociatedProfession { get; }
}
 
public abstract class HarvestGold : IHarvestResourceCommand
{
    public Resource ResourceToRecolt
    {
        get { return Resource.Gold; }
    }
 
    public abstract Profession AssociatedProfession { get; }
}
 
public abstract class HarvestStone : IHarvestResourceCommand
{
    public Resource ResourceToRecolt
    {
        get { return Resource.Stone; }
    }
 
    public abstract Profession AssociatedProfession { get; }
}

And now each command can inherit from the correct abstract class and just have to provide the profession associated to it (see some examples below).

public class FarmCrops : HarvestFood
{
    public override Profession AssociatedProfession
    {
        get { return Profession.Farmer; }
    }
}
 
public class CatchFish : HarvestFood
{
    public override Profession AssociatedProfession
    {
        get { return Profession.Fisherman; }
    }
}
 
public class CutTrees : HarvestWood
{
    public override Profession AssociatedProfession
    {
        get { return Profession.Lumberjack; }
    }
}
 
public class MineStone : HarvestStone
{
    public override Profession AssociatedProfession
    {
        get { return Profession.Miner; }
    }
}
 
public class MineGold : HarvestGold
{
    public override Profession AssociatedProfession
    {
        get { return Profession.Miner; }
    }
}

I can now update the implementation of the VillagerActor to use the properties available in the classes I just refactored to simplify it and to remove all the code duplication.

private void ResourceHarvester(IHarvestResourceCommand command)
{
    Profession = command.AssociatedProfession;
    ResourceToRecolt = command.ResourceToRecolt;
 
    _resourcesSupervisor.Tell(new ResourceRecolted { ResourceType = ResourceToRecolt, Quantity = 10 });
 
    ListenForCommands();
}
 
private void ListenForCommands()    // previously CommandsHandler()
{
    Receive<IHarvestResourceCommand>(m => Become(() => ResourceHarvester(m)));
}

In my opinion this is way cleaner than before even if the notion of one method by profession has been removed, we still have a property for this.

Now I think that I have a good base to add the child actor which will have a bit more logic than this current implementation.

The child actor

In Age Of Empires II, gathering resource takes time, a villager harvests resource one by one and brings them back to the a resource depot when his capacity is full. I will implement this logic in the new child actor called ResourceHarvesterActor. This actor is not a unit on its own, it will represent a subroutine of the villager.

public class ResourceHarvesterActor : TypedActor,
    IHandle<IHarvestResourceCommand>,
    IHandle<ResourceHarvesterActor.ResourceHarvested>
{
    private readonly ITellScheduler _messageScheduler;
    private readonly IActorRef _resourcesSupervisor;
 
    public const uint MAX_CAPACITY = 10;
    public uint CurrentlyCarrying { get; private set; }
 
    public Resource ResourceToHarvest { get; private set; }
 
    public ResourceHarvesterActor(ITellScheduler messageScheduler, IActorRef resourcesSupervisor)
    {
        _messageScheduler = messageScheduler;
        _resourcesSupervisor = resourcesSupervisor;
        CurrentlyCarrying = 0;
    }
 
    public void Handle(IHarvestResourceCommand message)
    {
        if (message.ResourceToRecolt != ResourceToHarvest)
        {
            ResourceToHarvest = message.ResourceToRecolt;
            CurrentlyCarrying = 0;
        }
        _messageScheduler.ScheduleTellOnce(TimeSpan.FromSeconds(1), Self, new ResourceHarvested(), Self);
    }
 
    public void Handle(ResourceHarvested message)
    {
        CurrentlyCarrying++;
        if (CurrentlyCarrying == MAX_CAPACITY)
            _resourcesSupervisor.Tell(new ResourceGathered { Quantity = CurrentlyCarrying, ResourceType = ResourceToHarvest });
        else
            _messageScheduler.ScheduleTellOnce(TimeSpan.FromSeconds(1), Self, new ResourceHarvested(), Self);
    }
 
    public class ResourceHarvested { }
}

To simulate the game logic, this actor will send a message to itself every second to increment the amount of resource the villager is carrying. To do so I use the ITellScheduler interface which is available in Akka.NET. This allows to set a delay before sending a message to an actor, in this case the actor is itself so I use the Self property. I chose to use this interface instead of an implementation to be able to inject the dependency to ease the testability of the class.

I also added a condition to detect when the villager is at full capacity, when it occurs then the ResourceSupervisorActor is used to increment the global counter for the resource being harvested (in the game the villager goes to the depot for this).

There is another game rule I implemented in this actor, the fact that a villager cannot gather several types a resource at the same time. For example if a lumberjack is ordered to go fishing, then the wood he is carrying is lost. I made a unit test to cover this case:

[Fact(DisplayName = "ResourceHarvesterActor Should Empty CurrentlyCarrying If Different Resource To Harvest")]
public void Empty_CurrentlyCarrying_If_Different_Resource_To_Harvest()
{
    _harvester.Tell(VillagerOrders.CutTrees);
    _harvester.Tell(new ResourceHarvesterActor.ResourceHarvested());
    _harvester.UnderlyingActor.CurrentlyCarrying.ShouldBe<uint>(1);
    _harvester.Tell(VillagerOrders.CatchFish);
    _harvester.UnderlyingActor.CurrentlyCarrying.ShouldBe<uint>(0);
}

Now I will use this new actor inside the VillagerActor.

public class VillagerActor : ReceiveActor
{
    private readonly IActorRef _resourcesSupervisor;
 
    public VillagerActor(IActorRef resourcesSupervisor)
    {
        _resourcesSupervisor = resourcesSupervisor;
        Profession = Profession.Idle;
 
        var props = Props.Create<ResourceHarvesterActor>(Context.System.Scheduler, _resourcesSupervisor);
        _resourceHarvesterRoutine = Context.ActorOf(props);
    }
 
    public Profession Profession { get; private set; }
    public Resource ResourceToRecolt { get; private set; }
 
    private readonly IActorRef _resourceHarvesterRoutine;
 
    protected override void PreStart()
    {
        base.PreStart();
        Become(Idle);
    }
 
    private void Idle()
    {
        ListenForCommands();
    }
 
    private void ResourceHarvester(IHarvestResourceCommand command)
    {
        Profession = command.AssociatedProfession;
        ResourceToRecolt = command.ResourceToRecolt;
 
        _resourceHarvesterRoutine.Tell(command);
 
        ListenForCommands();
    }
 
    private void ListenForCommands()
    {
        Receive<IHarvestResourceCommand>(m => Become(() => ResourceHarvester(m)));
    }
}

There is nothing particular in here except the fact that I used the Scheduler from the System (available through the Context) for the child actor. With the refactoring and the adding of a child actor, the VillagerActor has more the role of a coordinator than an actor since I remove all the logic from it. But this is a temporary situation, since I still have a lot of rules to implement, with time it will become a high level actor delegating work to child actors.

The code for the entire solution is available on my GitHub repository, it will allow you to see the entire project. Since I write the blog posts in parallel of the development of the solution itself it is likely that the code will change a lot with time due to constant refactoring. So it is possible that the code shown in the articles is not up-to-date when you read it, therefore do not hesitate the browse the changes in the repository’s history and to have a look at the different branches.

See you next time!

One thought on “Akka.NET: Delegating work to child actors

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s