Skip to main content

Fusion Character Controller

Quick Overview

This Character Controller design allows a character to switch control type from AI-Controlled to Player-Controlled easily. an example case is when a player disconnects, an AI will overtake its character control for a period of time till the player rejoin.

A simple illustration for character controller model.

Untitled

below are designed system for character controller.

Untitled

Determine Input Authority

to determine whether a network object should receive player input or AI input, simply check if the network object player has input authority (PlayerRef) if not, then allow AI to takeover it.

protected override void SetInputs()
        {
            if (!Runner.TryGetInputForPlayer(PlayerRef, out Inputs))
            {
                //for handling player object in remote, 
								//so AI will not try to control that object.
                if (!PlayerControlled)
                    AiInput?.SetInputs(out Inputs);
            }
        }

PlayerControlled is used to differentiate between player character objects in remote or a bot.

PlayerControlled = PlayerRef != default;

Setting up Identifier

Identifier mainly used to check if player should create a new character (Spawn) or reclaim already spawned (Takeover) when connect to a room (or a session, since in this research we do not create room system).

public class Player : NetworkBehaviour
    {
        [Networked] public string PlayerName { get; set; }

        public override void Spawned()
        {
            if (Object.HasInputAuthority)
            {
                Rpc_SetPlayerName(LauncherExtension.UI.InputNameField.text);
            }

            LauncherExtension.Launcher.SetPlayer(Object.InputAuthority, this);
        }

        [Rpc(sources: RpcSources.InputAuthority, targets: RpcTargets.StateAuthority)]
        public void Rpc_PlayerTakeoverBots()
        {
            LauncherExtension.Manager.RandomlyTakeoverAI(Object.InputAuthority);
        }

        [Rpc(sources: RpcSources.InputAuthority, targets: RpcTargets.StateAuthority)]
        public void Rpc_BotsTakeoverPlayer()
        {
            LauncherExtension.Manager.AiTakeOverPlayer(Object.InputAuthority);
        }

        [Rpc(sources: RpcSources.InputAuthority, targets: RpcTargets.StateAuthority)]
        private void Rpc_SetPlayerName(string playerName)
        {
            if (Runner.IsServer)
            {
                PlayerName = playerName;
            }
        }
    }

When a player successfully joins the session, the server will create its Identifier called Player Data. this player data hold PlayerName that the player should input before connecting to the server.

public virtual void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
        {
            if (!_players.ContainsKey(player))
                runner.Spawn(_playerDataPrefab, Vector3.zero, 
								Quaternion.identity, player);
        }

Similarly, the server will destroy the player’s Player Data when the player is disconnected.

public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
        {
            runner.Despawn(_players[player].Object);
            _players.Remove(player);
        }

Disconnect & Rejoin

when a player happens to disconnect in an unintended way, the server tries to get that player's character object and then releases the character from player authority. this way, it allows AI to take over its authority temporarily while players try to reconnect.

public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
        {
            if (LauncherExtension.Launcher.Runner.IsServer)
            {
                //get that newly disconnected player's character object.
                if (TryGetPlayer(player, out _tempPlayer))
                {
                    //remove player authority from character.
                    _tempPlayer.Object.RemoveInputAuthority();
                    //refresh newly authority in character object.
                    _tempPlayer.RefreshPlayerRef();
                }
            }
        }

whether player reconnects or joins for the first time, server will check player ‘identifier’ to tell whether player's character is already in the match or not.

public virtual async void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
        {
            //giving character spawner a little delay so player data spawner can finish set up.
            await Task.Delay(300);

            if (LauncherExtension.Launcher.Runner.IsServer)
            {
                //check if character object is already spawned by using name as keys.
                if (TryGetPlayer(LauncherExtension.Launcher.GetPlayer(player).PlayerName, out _tempPlayer))
                {
                    //assign input authority if player rejoin.
                    _tempPlayer.Object.AssignInputAuthority(player);
                    //refresh input authority in player object so it sync with newly assigned player.
                    _tempPlayer.RefreshPlayerRef();
                }
                else
                    //if player result null, create new one.
                    LauncherExtension.Launcher.Runner.Spawn(_unitPrefab, Vector3.zero, Quaternion.identity, player);
            }
        }

Takeover & Release Bots

Takeover & Release Player Authority in Bots works similarly to Rejoin & Disconnect. when a player wants to take over a random bot, the server will find a random bot that does not have input authority, then assign player authority to that bot.

public void RandomlyTakeoverAI(PlayerRef player)
        {
            //get character who does not have authority.
            if (TryGetAI(out _tempPlayer))
            {
                if (LauncherExtension.IsServer)
                {
                    //assign authority to that character.
                    _tempPlayer.Object.AssignInputAuthority(player);
                    //refresh newly authority in character.
                    _tempPlayer.RefreshPlayerRef();
                }
            }
        }

while releasing player authority toward the bot simply by removing player authority to that bot.

public void AiTakeOverPlayer(PlayerRef player)
        {
            var _players = Player.FindAll(x => x.PlayerRef == player);

            //prevent when only one character trying to remove its authority.
            if (_players.Count == 1) return;

            //get character who requesting to remove its authority.
            if (TryGetPlayer(player, out _tempPlayer))
            {
                if (LauncherExtension.IsServer)
                {
                    //remove character authority.
                    _tempPlayer.Object.RemoveInputAuthority();
                    //refresh newly authority in character.
                    _tempPlayer.RefreshPlayerRef();
                }
            }
        }

Research Notes

  • In Server Mode, there is no default Player Ref for spawned object who does not define its Player. so their player authority set to None.
  • This system works around giving and removing authority upon network objects. an object who does not have authority but has a unit controller component attached, is considered as bot.
  • In this research; PlayerName used as identifier to determine if character already spawned or not. this enable player to rejoin and recover its character authority. however, in real environment, kindly consider using much better and secure way to identify character.
  • Below a quick demo of character controller in action.

Repository

Check here for repos implementation.