Votre premier mode de jeu
Créez pas à pas un petit mode de jeu deathmatch. À la fin de ce tutoriel, vous saurez :
- Créer et configurer un Package de type game-mode
- Organiser votre code dans les dossiers
Server/,Client/etShared/ - Faire apparaître les joueurs aux points d'apparition de la map et les faire réapparaître
- Utiliser un autre Package (default-weapons) comme dépendance
- Garder un score synchronisé avec tous les joueurs
- Afficher un HUD simple sur le client
- Envoyer un Remote Event depuis le client et le valider sur le serveur
Ce tutoriel fait suite au démarrage rapide. Si vous ne l'avez pas encore suivi, commencez par là pour configurer votre serveur !
Étape 1 : créer le Package
Créons un nouveau Package avec la CLI, cette fois de type game-mode :
./NanosWorldServer.exe --cli add package my-deathmatch
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
Cela crée le dossier Packages/my-deathmatch/ avec les dossiers Server/, Client/ et Shared/, et un Package.toml.
Un game-mode est un Package comme un script, à ceci près qu'un seul game-mode peut être chargé à la fois. C'est le Package « principal » de votre serveur, et il apparaît dans l'écran New Game du jeu.
Étape 2 : configurer le Package.toml
Ouvrez le Package.toml. Il contient une section [meta] avec les informations de votre Package, et une section [game_mode] avec ses réglages. Il faut faire deux changements :
- Ajoutez
default-weaponsauxpackages_requirementspour pouvoir faire apparaître ses armes. Il sera chargé avant notre Package. - Ajoutez
default-playground-mapàcompatible_maps, pour qu'elle soit proposée en lançant une nouvelle partie.
[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-playground-map",
]
Définissez-le ensuite comme game-mode de votre serveur, dans le Config.toml :
[game]
map = "default-playground-map"
game_mode = "my-deathmatch"
# remove "my-awesome-package" from the Quick Start, if you want
packages = [
]
Si default-weapons n'est pas installé sur votre serveur, téléchargez-le avec ./NanosWorldServer.exe --cli install package default-weapons, ou démarrez le serveur avec --auto_download.
Étape 3 : réglages partagés
Le code de Shared/ s'exécute à la fois sur le serveur et sur les clients. C'est l'endroit idéal pour les réglages que les deux côtés doivent connaître. Nous n'y stockerons que des valeurs, sans jamais faire apparaître d'entités (sinon chaque côté ferait apparaître sa propre copie !) :
-- Game settings, available on both Server and Client
KILLS_TO_WIN = 10
RESPAWN_TIME = 3000 -- milliseconds
Étape 4 : faire apparaître et réapparaître les joueurs
Sur le serveur, nous donnons à chaque joueur un Character avec une arme, à un point d'apparition aléatoire de la map, et le faisons réapparaître quelques secondes après sa mort :
-- 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)
Faisons maintenant réapparaître les Characters après leur mort. Le Character reste dans le monde (en ragdoll) quand il meurt : il suffit donc d'appeler Respawn dessus un peu plus tard :
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)
Rechargez votre Package avec package reload my-deathmatch et rejoignez le serveur : vous apparaissez avec un fusil et réapparaissez après votre mort !
Envie de tester seul ? Faites apparaître un PNJ sur qui tirer : Character(Vector(300, 0, 100), Rotator(), "nanos-world::SK_Male").
Étape 5 : compter les points
Comptons les kills de chaque joueur. Nous les stockerons comme valeur synchronisée sur le Player, pour que tous les clients connaissent le score de chacun, y compris les joueurs qui arrivent plus tard.
Modifiez l'événement Death pour donner un point au tueur :
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
Étape 6 : afficher le HUD
Côté client maintenant, affichons à l'écran les kills du joueur local avec un Canvas. Comme la valeur est synchronisée, le client peut la lire directement avec GetValue :
-- 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)
Remarquez que nous avons utilisé KILLS_TO_WIN du fichier Shared/Index.lua, disponible des deux côtés !
Pour des interfaces plus complexes, vous pouvez utiliser du HTML/CSS/JavaScript avec une WebUI. Voir HUD basique (HTML).
Étape 7 : envoyer une action depuis le client
Enfin, permettons aux joueurs de narguer en appuyant sur une touche. Les entrées n'existent que côté client : le client doit donc demander au serveur de jouer l'animation, qui la synchronise ensuite pour tout le monde.
Côté client, nous enregistrons un raccourci clavier (les joueurs peuvent changer la touche dans les paramètres) et envoyons un Remote Event quand il est pressé :
-- 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)
Côté serveur, nous le recevons. Rappel : ne faites jamais confiance au client ! Un tricheur pourrait appeler cet événement autant de fois qu'il veut : nous vérifions donc que le joueur peut narguer maintenant et limitons la fréquence :
-- 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
Félicitations, vous avez créé votre premier game-mode ! 🎉 Vous connaissez maintenant les bases de tout game-mode : faire apparaître et réapparaître les joueurs, utiliser des dépendances, synchroniser des données, afficher une interface et faire communiquer client et serveur.
Quelques idées pour l'améliorer :
- Afficher un tableau des scores avec les kills de tous les joueurs (parcourez
Player.GetPairs()côté client). - Donner des armes différentes aux joueurs, parmi celles de default-weapons.
- Ajouter des équipes avec Failed to load function data..
- Sauvegarder les meilleurs joueurs avec les données persistantes.
Continuez à apprendre avec les concepts clés utilisés dans ce tutoriel :
Cycle de vie du joueurcore-concepts/scripting/player-lifecycle Réseau et réplicationcore-concepts/scripting/networking-and-replication Ne jamais faire confiance au clientcore-concepts/scripting/security-remote-events Entrées et raccourcis claviercore-concepts/scripting/input-and-key-bindings Chargement des Packages et environnement Luacore-concepts/packages/package-loading-and-lua-environment