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.
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.
| Callback | When |
|---|---|
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 } } }
The session
| C# | Does |
|---|---|
GameSession.Players, Player.Name, Entity, NetId, IsReady, IsBot | Clients and bots in join order |
GameSession.SetReady, Kick | Ready flags; disconnect a client or remove a bot |
GameSession.Travel("Scenes/Next.kscene") | Change the map before the next frame |
GameSession.MatchState | A string your game defines; clients receive it, also when they join |
GameSession.GetSetting(key, fallback) | Rules from GameRules.kserverrules and --rule |
GameSession.AddBot, RemoveBot | Server bots |
The session is server-only: GameSession.IsAuthority tells, and RequireAuthority() throws elsewhere.
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}"); } }
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. } }
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.