Best Way to Create Control-Agnostic Character?

Godot Version

4.6

Question

Hello, I was wondering if anyone had advice on how to structure a character that can EITHER be controlled by an AI, or by a player’s inputs?

Right now I have a working solution, where the “character” node has public variables (move vector, look vector, etc), and then I add a child “controller” node that’s either AI or player that changes those variables. I am wondering if this is a good long term solution, and if there’s any other ways to do it? I’m aware of scene inheritance but I’m not sure if it can be done during runtime, because I would like the ability to dynamically switch the character between player or AI control 24/7. Thank you!

The absolute best way that I can think of is by using the Command pattern.

But to give you a simple example (sorry it’s gonna be in C# since that’s what I use the most, but you can easily apply it to GDScript), here goes.

You can have an interface that has nothing but a single function, in most cases, called Execute

public interface ICommand
{
    public void Execute(double delta);
}

Then, you can have specific commands that will implement that interface, like so:

public class MoveUpCommand : ICommand
{
    private readonly Actor _actor;
    public MoveUpCommand(Actor actor)
    {
        _actor = actor;
    }

    public void Execute(double delta)
    {
        _actor.MoveUp(delta);
    }
}

And this is where the magic happens. Your player / AI object should only have functions for moving around, but nothing to do with Inputs.

public void MoveUp(double delta)
    {
        GlobalPosition += Vector2.Up * _movementSpeed * (float)delta;
    }

    public void MoveDown(double delta)
    {
        GlobalPosition += Vector2.Down * _movementSpeed * (float)delta;
    }

    public void MoveLeft(double delta)
    {
        GlobalPosition += Vector2.Left * _movementSpeed * (float)delta;
    }

    public void MoveRight(double delta)
    {
        GlobalPosition += Vector2.Right * _movementSpeed * (float)delta;
    }

And in another script, you can simply handle JUST inputs / movement requests, for the player specifically, I did this in this example:

public partial class InputController : Node
{
    private System.Collections.Generic.Dictionary<StringName, ICommand> _bindings;
    public IReadOnlyDictionary<StringName, ICommand> Bindings => _bindings;
    private PlayerScript _player;
    private bool _initialized;


    public override void _Ready()
    {
        base._Ready();
        _bindings = new System.Collections.Generic.Dictionary<StringName, ICommand>();
        _player = GetParent().GetNode<PlayerScript>("Player");

        SetupDefaultBindings();
    }


    private void SetupDefaultBindings()
    {
        if (_player == null)
        {
            GD.PushWarning("We couldn't find the player, something is wrong!");
            return;
        }

        _initialized = true;

        Array<StringName> actions = InputMap.GetActions();
        foreach (StringName action in actions)
        {
            if (action.ToString().Contains("ui"))
            {
                continue;
            }

            switch (action)
            {
                case "Up":
                    BindInputEventToCommand(action, new MoveUpCommand(_player));
                    break;
                case "Down":
                    BindInputEventToCommand(action, new MoveDownCommand(_player));
                    break;
                case "Left":
                    BindInputEventToCommand(action, new MoveLeftCommand(_player));
                    break;
                case "Right":
                    BindInputEventToCommand(action, new MoveRightCommand(_player));
                    break;
            }
        }

        GD.Print("Bindings set.");
    }

    private void BindInputEventToCommand(StringName @event, ICommand command)
    {
        if (!_bindings.TryAdd(@event, command))
        {
            GD.PushWarning("Could not bind event, likely duplicate: " + @event);
        }
    }

    public override void _Process(double delta)
    {
        base._Process(delta);

        if (!_initialized)
        {
            return;
        }

        foreach (KeyValuePair<StringName, ICommand> kv in _bindings)
        {
            if (Input.IsActionPressed(kv.Key))
            {
                kv.Value.Execute(delta);
            }
        }
    }
}

The most important bit is the last _Process function, that will run all the time and check if a given input that we store in the Dictionary matches one of the Input methods. And since the Dictionary contains a key (in this case, that’s a string), and a value (in this case, that’s a Command that has the ICommand interface implemented), we simply call the Execute function when we KNOW that specific input is being pressed that we assigned to that command.

This way, Player or AI actors know absolutely nothing about how the input system work, and the Input system itself has no clue who or what it’s controlling, it just knows there’s an Execute function that needs to be called when X or Y thing happens.

4 Likes

Thank you for such a detailed response, I really appreciate it! Yes, your solution looks great. Where exactly would you store the player input / AI scripts? Would they be attached to a node or global scripts? Sorry, not sure if the terms are different for C#.

C# works the same way in that regard to GDScript, you just attach the script to a Node. And where you keep the player input or AI script would depend entirely on how you want to structure your project. In my example I simply put it on a Node on my example scene, then get the reference to the things I needed.

2 Likes

Okay, I understand. I wasn’t sure if the “controller” script should be attached to a node, and the character is parented to that node, or if its “bad practice” to have a child node of the character be the controller node that interfaces with its parent. Wanting the ability to hot-swap between player and AI complicates it slightly.