KROYSDOCS
HomeWhat's newv0.1.0
MANUAL / Multiplayer / Game modes and bots

Game modes and bots

A game's rules are a C# class that runs on the dedicated server: it sees players join and leave, owns the match state, travels between maps and drives bots that move like players.

Base classGameMode
SessionGameSession, Player
BotsBot
TemplateGame Module
01

A game mode

Derive from GameMode and start the server with --game-mode Namespace.Type. One instance runs per server; it is not attached to an entity. It receives the session events before its OnFixedUpdate each server update.

CallbackWhen
OnPlayerJoined(player)A client connected or a bot was added
OnPlayerLeft(player)A client disconnected or was kicked, or a bot was removed
OnPlayerReadyChanged(player, ready)A player became ready or not
OnTravelled(map)The server loaded another map; players are at its spawn points
OnMatchStateChanged(state)The match state changed
DuelMode.cs
public sealed class DuelMode : GameMode
{
    protected override void OnStart()
    {
        GameSession.MatchState = "Warmup";
        Player bot = GameSession.AddBot("Sparring Bot");   // a server player, replicated like a client
        GameSession.SetReady(bot, true);
    }

    protected override void OnPlayerJoined(Player player) => Debug.Log($"{player.Name} joined");
    protected override void OnPlayerLeft(Player player) => Debug.Log($"client {player.ClientId} left");

    protected override void OnPlayerReadyChanged(Player player, bool ready)
    {
        if (GameSession.MatchState == "Warmup" && GameSession.AllPlayersReady)
            GameSession.MatchState = "Playing";
    }

    protected override void OnFixedUpdate(float fixedDeltaTime)
    {
        foreach (Player player in GameSession.Players)
        {
            if (player.IsBot)
                Bot.MoveToward(player, Vector3.Zero, 1.0f);   // the same character step as clients
        }
    }
}
02

The session

C#Does
GameSession.Players, Player.Name, Entity, NetId, IsReady, IsBotClients and bots in join order
GameSession.SetReady, KickReady flags; disconnect a client or remove a bot
GameSession.Travel("Scenes/Next.kscene")Change the map before the next frame
GameSession.MatchStateA string your game defines; clients receive it, also when they join
GameSession.GetSetting(key, fallback)Rules from GameRules.kserverrules and --rule
GameSession.AddBot, RemoveBotServer bots

The session is server-only: GameSession.IsAuthority tells, and RequireAuthority() throws elsewhere.

03

Clients take part

Clients use ordinary script messages: GameSession.RequestReady(true) sends the ready flag; TryReadMatchState and TryReadTravel read the server's messages in OnNetworkMessage. Message names starting with Kroys. belong to the engine; name your own after the game (Match.*, Craft.*).

LobbyClient.cs
[ClientOnly]
public sealed class LobbyClient : EntityScript
{
    protected override void OnUpdate(float deltaTime)
    {
        if (Network.IsConnected && Input.IsKeyPressed(KeyCode.R))
            GameSession.RequestReady(true);   // sends Kroys.Session.Ready to the server
    }

    protected override void OnNetworkMessage(NetworkMessage message)
    {
        if (GameSession.TryReadMatchState(message, out string state))
            Debug.Log($"match: {state}");
        else if (GameSession.TryReadTravel(message, out string map))
            Debug.Log($"server travelled to {map}");
    }
}
04

Client, server, or both

The game has one script assembly. Mark presentation-only scripts (cameras, HUD, input, audio, effects) [ClientOnly]: the server refuses them. Mark rules [ServerOnly] (every GameMode already is): Play mode and clients refuse them. Unmarked scripts run everywhere; branch with Network.IsServer where a shared script must behave differently.

Pickup.cs
// Runs in the editor's Play mode, in the game and on the dedicated server.
public sealed class Pickup : EntityScript
{
    protected override void OnTriggerEnter(Entity other)
    {
        if (Network.IsServer)
        {
            // Server: the authority decides; the removal replicates to every client.
            Entity.Destroy();
        }
        else if (!Network.IsClient)
        {
            // Single player and Play mode.
            Entity.Destroy();
        }
        // Clients wait for the server.
    }
}
05

Bots

A bot is a server player driven by game code. It applies its input every server tick through the same character step as client players, so it collides with the level and replicates like them. Bot.MoveToward steers toward a point; Bot.SetInput sets move axes, facing, jump and sprint directly. Bake a navmesh for the level and bots can follow paths: the server cook ships it and Navigation.FindPath answers on the server.

The Game Module project template ships a complete example: a warmup until every human is ready, a timed match, travel to the next map, and bots that patrol and chase.

ChaseBots.cs
public sealed class ChaseBots : GameMode
{
    private readonly List<Vector3> path = new();

    protected override void OnStart()
    {
        GameSession.AddBot("Hunter");
    }

    protected override void OnFixedUpdate(float fixedDeltaTime)
    {
        Player? target = GameSession.Players.FirstOrDefault(p => !p.IsBot);
        foreach (Player bot in GameSession.Players.Where(p => p.IsBot))
        {
            if (target is not Player human)
            {
                Bot.Stop(bot);
                continue;
            }
            Vector3 from = bot.Entity.Transform.WorldPosition;
            Vector3 to = human.Entity.Transform.WorldPosition;
            // With a baked navmesh, walk the path's corners; without one, head straight there.
            Vector3 next = to;
            if (Navigation.IsReady && Navigation.FindPath(from, to, path) != NavPathStatus.Failed && path.Count > 1)
                next = path[1];
            Bot.MoveToward(bot, next, arriveDistance: 1.0f, sprint: Vector3.Distance(from, to) > 8.0f);
        }
    }
}

KroysEngine 0.1.0 documentation, built from the engine repository by Tools/BuildDocsSite.py.