04-07-2012, 03:01 PM
Je voudrais vous faire part d'un petit compte rendu de mes recherches récentes sur l'architecture client-serveur d'un jeu en ligne. La façon la plus commune de procéder est d'envoyer un message au serveur lorsqu'un évènement est déclenché côté client (par exemple appuyer sur la flèche haut), afin que celui-ci traite l'information et renvoie une réponse à l'ensemble des clients y compris le client qui a déclenché l'évènement.
Cependant en cas de lag, le jeu se retrouve "freezé", votre personnage marche comme un robot, se teleporte ect. C'est le cas dans Counter Strike par exemple. Dans d'autres jeux, même lorsqu'il y a du lag, vous voyez votre personnage avancer normalement. C'est le cas dans Gears of War sur Xbox par exemple (bon c'est mon jeu Xbox préféré donc je le cite ). Lorsqu'il y a un décalage trop important, votre personnage se retrouve déporté en arrière, il retrouve son état de quelques secondes auparavant.
Ce système permet de cacher en partie le lag, même si bien sûr ça ne résout pas le problème. C'est moins frustrant pour le joueur de pouvoir encore faire quelque chose malgré le lag que se retrouver totalement bloqué. C'est se qu'on appelle la prédiction (coté client).
Pour en discuter, je propose une implémentation pour NodeJS et Socket.IO (oui je sais encore iffle.
Cependant en cas de lag, le jeu se retrouve "freezé", votre personnage marche comme un robot, se teleporte ect. C'est le cas dans Counter Strike par exemple. Dans d'autres jeux, même lorsqu'il y a du lag, vous voyez votre personnage avancer normalement. C'est le cas dans Gears of War sur Xbox par exemple (bon c'est mon jeu Xbox préféré donc je le cite ). Lorsqu'il y a un décalage trop important, votre personnage se retrouve déporté en arrière, il retrouve son état de quelques secondes auparavant.
Ce système permet de cacher en partie le lag, même si bien sûr ça ne résout pas le problème. C'est moins frustrant pour le joueur de pouvoir encore faire quelque chose malgré le lag que se retrouver totalement bloqué. C'est se qu'on appelle la prédiction (coté client).
Pour en discuter, je propose une implémentation pour NodeJS et Socket.IO (oui je sais encore iffle.
###
# Client
# require jQuery, underscore
###
# [...]
$ ->
players = []
socket = io.connect '/?id=1'
client =
id: 1
x : 0
y : 0
hp: 100
# Récupération des joueurs depuis le serveur
socket.on 'sendPlayers', (pPlayers) ->
players.push player for player in JSON.parse pPlayers
client.move = (x, y) ->
# On fait la modif coté client tout de suite
# Sans la prédiction, on aurait attendu la réponse du server d'abord
client.x = x
client.y = y
# Animation...
# On avertit le server s'il s'agit du client
# => dans un cas réel, la méthode move() vient d'une classe Player
# => pour l'exemple j'ai abrégé en client.move()
socket.emit 'move', client if this is client
# Si le server nous avertir du déplacement d'un joueur
socket.on 'move', (pPlayer) ->
for player in players when player.id is pPlayer.id
player.move pPlayer.x, pPlayer.y
client.attack = (target) ->
# On fait la modif coté client tout de suite
# Sans la prédiction, on aurait attendu la réponse du server d'abord
target.hp -= 10
# Animation...
# On avertit le server s'il s'agit du client
# => dans un cas réel, la méthode attack() vient d'une classe Player
# => pour l'exemple j'ai abrégé en client.attack()
socket.emit 'attack', target if this is client
# Si le server nous avertit de l'attaque d'un joueur
socket.on 'attack', (pPlayer, pTarget) ->
for player in players when player.id is pPlayer.id
player.attack pTarget
# On vérifie si l'état du jeu côté client et le même que coté server
# Si ça n'est pas le cas, on rétablit l'état du jeu comme sur le server
# Note : pas optimisé car peut obliger à renvoyer beaucoup trop de données à intervalle régulier
# On pourrait stocker côté client l'état du jeu dans une pile puis restituer selon un timestamp ou un ID incrémenté au fur et à mesure des actions...
socket.on 'checkState', (pPlayers) ->
sameState = yes
sameState = _.isEqual(player, players[i]) and sameState for player, i in JSON.parse pPlayers
if sameState is no
players = []
players.push player for player in JSON.parse pPlayers
###
# Server
# require Socket.IO
###
# [...]
players = []
client = null
io = require('socket.io').listen 80
# Connexion à Socket.IO
io.sockets.on 'connection', (socket) ->
# On définit l'état du client
client =
id: socket.handshake.query.id #/?query=id
x : 0
y : 0
hp: 100
players.push client
# On envoie l'état des joueurs au client (pas optimisé si on a beaucoup de joueurs => /!\ bande passante)
socket.emit 'sendPlayers', JSON.stringify players # (JSON.stringify Array) => JSON
# Mise à jour coordonnées du joueur
socket.on 'move', (pPlayer) ->
# Vérifications préalables : assez d'énergie, client devant la cible, pas d'obstacle ect...
for player in players when player.id is pPlayer.id
# Note : on peut factoriser le code et partager les méthodes de classe entre le client et le serveur
player.x = pPlayer.x
player.y = pPlayer.y
# Note : broadcast envoie à tout le monde sauf le client
socket.broadcast.emit 'move', pPlayer
# Attaque d'un joueur
socket.on 'attack', (target) ->
# Vérifications préalables : assez d'énergie, client devant la cible, pas d'obstacle ect...
for player in players when player.id is target.id
player.hp -= 10
# Note : broadcast envoie à tout le monde sauf le client
socket.broadcast.emit 'attack', client, target
# Pour l'exemple, j'utilise un timer de 1 seconde
# Meilleure implémentation : faire la vérif à chaque fois qu'on reçoit un ping
setInterval ->
socket.emit 'checkState', JSON.stringify players # (JSON.stringify Array) => JSON
, 1000