Skip to main content
Version: latest - a1.156.0 โš–๏ธ

Your First Game-Mode

Build a small deathmatch game-mode step by step. After this tutorial you will know how to:

  1. Create and configure a game-mode Package
  2. Organize code in Server/, Client/ and Shared/ folders
  3. Spawn players at the map spawn points and respawn them
  4. Use another Package (default-weapons) as a requirement
  5. Keep a score synchronized with all players
  6. Draw a simple HUD on the client
  7. Send a Remote Event from the client, and validate it on the server
tip

This tutorial continues from the Quick Start. If you haven't done it yet, start there to set up your server!

Step 1: Creating the Packageโ€‹

Let's create a new Package with the CLI, this time of type game-mode:

Terminal
./NanosWorldServer.exe --cli add package my-deathmatch
NanosWorldServer
INFO Please enter the Package Title: (my-deathmatch)
My Deathmatch
INFO Please enter the Package Author: ()
myself
INFO Please enter the Package Type ('script', 'game-mode', 'map' or 'loading-screen'): (game-mode)
game-mode

This creates the folder Packages/my-deathmatch/ with the Server/, Client/ and Shared/ folders, and a Package.toml.

info

A game-mode is a Package just like a script, with the difference that only one game-mode can be loaded at a time. It's the "main" Package of your server, and it appears in the New Game screen of the game.

Step 2: Configuring the Package.tomlโ€‹

Open the Package.toml. It contains a [meta] section with the information of your Package, and a [game_mode] section with its settings. We need two changes:

  • Add default-weapons to packages_requirements, so we can spawn its weapons. It will be loaded before our Package.
  • Add default-testing-map to compatible_maps, so it's suggested when starting a new game.
Packages/my-deathmatch/Package.toml
[meta]
title = "My Deathmatch"
author = "myself"
version = "0.0.1"

[game_mode]
# ... keep the other settings generated by the CLI ...
# packages requirements
packages_requirements = [
"default-weapons",
]
# compatible maps - maps to be highlighted when starting a new game through main menu
compatible_maps = [
"default-testing-map",
]

Now set it as the game-mode of your server, in the Config.toml:

Config.toml
[game]
map = "default-testing-map"
game_mode = "my-deathmatch"
# remove "my-awesome-package" from the Quick Start, if you want
packages = [
]
tip

If default-weapons is not installed in your server, download it with ./NanosWorldServer.exe --cli install package default-weapons, or start the server with --auto_download.

Step 3: Shared Settingsโ€‹

Code in Shared/ runs on both the server and the clients. It's the perfect place for settings both sides need to know. We will only store values here, never spawn entities (otherwise each side would spawn its own copy!):

Packages/my-deathmatch/Shared/Index.lua
-- Game settings, available on both Server and Client
KILLS_TO_WIN = 10
RESPAWN_TIME = 3000 -- milliseconds

Step 4: Spawning and Respawning Playersโ€‹

On the server, we give each player a Character with a weapon at a random spawn point of the map, and respawn it some seconds after dying:

Packages/my-deathmatch/Server/Index.lua
-- Returns a random spawn point of the current map
function GetRandomSpawnPoint()
local spawn_points = Server.GetMapSpawnPoints()

-- Falls back to the center of the map if the map doesn't have spawn points
if (#spawn_points == 0) then
return Vector(0, 0, 100), Rotator()
end

local spawn_point = spawn_points[math.random(#spawn_points)]
return spawn_point.location, spawn_point.rotation
end

-- Gives a weapon to a Character
function GiveWeapon(character)
-- AR4 is a class defined by the default-weapons Package
local weapon = AR4(character:GetLocation(), Rotator())
character:PickUp(weapon)
end

-- Spawns a Character for a Player
function SpawnPlayer(player)
local location, rotation = GetRandomSpawnPoint()

local character = Character(location, rotation, "nanos-world::SK_Male")
player:Possess(character)

GiveWeapon(character)
end

-- When a Player joins, spawns their Character
Player.Subscribe("Spawn", SpawnPlayer)

-- When the Package (re)loads, spawns Characters for Players already connected
Package.Subscribe("Load", function()
for _, player in pairs(Player.GetPairs()) do
if (not player:GetControlledCharacter()) then
SpawnPlayer(player)
end
end
end)

-- When a Player leaves, destroys their Character
Player.Subscribe("Destroy", function(player)
local character = player:GetControlledCharacter()
if (character) then
character:Destroy()
end
end)

Now let's respawn Characters after they die. The Character stays in the world (in ragdoll) when it dies, so we just need to call Respawn on it later:

Packages/my-deathmatch/Server/Index.lua
Character.Subscribe("Death", function(character, last_damage_taken, last_bone_damaged, damage_type_reason, hit_from_direction, instigator)
Timer.SetTimeout(function()
-- The Character may have been destroyed in the meantime (e.g. the Player left)
if (not character:IsValid()) then return end

local location, rotation = GetRandomSpawnPoint()
character:Respawn(location, rotation)

-- The weapon was dropped when dying, so we give a new one
GiveWeapon(character)
end, RESPAWN_TIME)
end)

Reload your Package with package reload my-deathmatch and join the server: you will spawn with a rifle, and respawn after dying!

tip

Want to test it alone? Spawn a NPC to shoot at: Character(Vector(300, 0, 100), Rotator(), "nanos-world::SK_Male").

Step 5: Keeping the Scoreโ€‹

Let's count the kills of each player. We will store it as a synchronized value on the Player, so all clients know the score of everyone, including players who join later.

Update the Death event to give a point to the killer:

Packages/my-deathmatch/Server/Index.lua
Character.Subscribe("Death", function(character, last_damage_taken, last_bone_damaged, damage_type_reason, hit_from_direction, instigator)
-- 'instigator' is the Player who caused the damage (it can be nil, e.g. when falling)
local victim = character:GetPlayer()

if (instigator and instigator ~= victim) then
local kills = instigator:GetValue("kills", 0) + 1

-- 'true' synchronizes the value with all clients
instigator:SetValue("kills", kills, true)

if (kills >= KILLS_TO_WIN) then
EndRound(instigator)
end
end

-- ... the respawn Timer from the previous step ...
end)

-- Announces the winner and resets everyone's score
function EndRound(winner)
Chat.BroadcastMessage(winner:GetName() .. " won the round!")

for _, player in pairs(Player.GetPairs()) do
player:SetValue("kills", 0, true)
end
end

Step 6: Drawing the HUDโ€‹

Now on the client, let's display the local player's kills on the screen using a Canvas. Since the value is synchronized, the client can read it directly with GetValue:

Packages/my-deathmatch/Client/Index.lua
-- Spawns a Canvas which only repaints when we ask (auto_repaint_rate = -1)
HUD = Canvas(true, Color.TRANSPARENT, -1, true)

HUD:Subscribe("Update", function(self, width, height)
local local_player = Client.GetLocalPlayer()
if (not local_player) then return end

local kills = local_player:GetValue("kills", 0)
self:DrawText("Kills: " .. kills .. " / " .. KILLS_TO_WIN, Vector2D(50, height - 80), FontType.OpenSans, 24, Color.WHITE)
end)

-- Repaints the HUD when any Player value changes (e.g. the kills)
Player.Subscribe("ValueChange", function(player, key, value)
if (key == "kills" and player == Client.GetLocalPlayer()) then
HUD:Repaint()
end
end)

-- Also repaints when the local Player spawns and when the screen is resized
Client.Subscribe("SpawnLocalPlayer", function(local_player)
HUD:Repaint()
end)

Viewport.Subscribe("Resize", function(new_size)
HUD:Repaint()
end)

Note how we used KILLS_TO_WIN from the Shared/Index.lua file, which is available on both sides!

tip

For more complex UIs, you can use HTML/CSS/JavaScript with a WebUI. See Basic HUD (HTML).

Step 7: Sending an Action from the Clientโ€‹

Finally, let's allow players to taunt by pressing a key. Input only exists on the client, so the client must ask the server to play the animation, which then synchronizes it to everyone.

On the client, we register a Key Binding (players can change the key in the settings) and send a Remote Event when it's pressed:

Packages/my-deathmatch/Client/Index.lua
-- Registers a Key Binding called "Taunt", using the key T by default
Input.Register("Taunt", "T", "Taunt")

Input.Bind("Taunt", InputEvent.Pressed, function()
-- Asks the server to taunt
Events.CallRemote("Taunt", Reliability.Reliable)
end)

On the server, we receive it. Remember: never trust the client! A cheater could call this event as many times as they want, so we check if the player can taunt right now and limit how often:

Packages/my-deathmatch/Server/Index.lua
-- Stores the last time each Player taunted
local last_taunt_time = {}

-- On the server, the first parameter of a Remote Event is always the Player who sent it
Events.SubscribeRemote("Taunt", function(player)
-- Validates the player has a living Character
local character = player:GetControlledCharacter()
if (not character or character:IsDead()) then return end

-- Allows only one taunt every 3 seconds
local now = Server.GetTime()
if (last_taunt_time[player] and now - last_taunt_time[player] < 3000) then return end
last_taunt_time[player] = now

-- Called on the server: every player will see the animation
character:PlayAnimation("nanos-world::A_Mannequin_Taunt_Bow", AnimationSlotType.FullBody)
end)

Player.Subscribe("Destroy", function(player)
last_taunt_time[player] = nil
end)

Conclusionโ€‹

Congratulations, you created your first game-mode! ๐ŸŽ‰ You now know the basics used by any game-mode: spawning and respawning players, using requirements, synchronizing data, drawing UI and communicating between the client and the server.

Some ideas to keep improving it:

  • Show a scoreboard with the kills of all players (iterate Player.GetPairs() on the client).
  • Give players different weapons, from the ones available in default-weapons.
  • Add teams with :SetTeam().
  • Save the best players with Persistent Data.

Continue learning with the core concepts used in this tutorial:

Player Lifecyclecore-concepts/scripting/player-lifecycle Networking & Replicationcore-concepts/scripting/networking-and-replication Never Trust the Clientcore-concepts/scripting/security-remote-events Input & Key Bindingscore-concepts/scripting/input-and-key-bindings Package Loading & Lua Environmentcore-concepts/packages/package-loading-and-lua-environment