Ars Tactica est un jeu de stratégie au tour par tour qui oppose deux ou plusieurs joueurs dont l’objectif est de détruire la base de leur adversaire(s).
Le jeu oppose deux camps (1v1 ou 2v2) dans un match à mort pour la destruction de tous les bâtiments affectés à une équipe, ceci en tour par tour. Chaque joueur contrôle un bâtiment principal qui lui sert de point de départ pour produire des unités de combat ainsi que des bâtiments collecteurs de ressources qui lui permettent d’augmenter sa production d’unités. Le joueur devra aussi investir des ressources dans des technologies qui lui débloqueront l’accès à des différents types d’unité.
Chaque joueur commence la partie avec un bâtiment principal permettant de créer des unités. Un joueur est éliminé de la partie lorsque tous ses bâtiments sont détruits. L'ordre de passage des joueurs est déterminé aléatoirement lors du début de la partie.
Au début du tour d'un joueur, toutes les unités en cours de production dont le temps de production restant
arrive à zero sont placés sur la carte le plus proche possible de leur bâtiment producteur. Le joueur peut
ensuite déplacer et controller ses unités sur la carte. Chaque unité peut être déplacée une fois par tour et peut
attaquer une autre unité/bâtiment un nombre de fois par tour correspondant à sa valeur Spd
(Vitesse).
Un joueur peut aussi demander la production d'unités par ses bâtiments (chaque bâtiment possède un
certain nombre de modules de production parallèles spécifiés par sa valeur Prod
).
Lorsque le joueur à terminé ses actions, il passe alors sont tour au joueur suivant dans l'ordre de passage.
Nom | Paramètres | Description |
---|---|---|
Actor | Type , Hp , Cost | Archétype de base pour toute entité active dans la partie (unités, bâtiments) |
Movable | Mov | Permet à l'entité de se déplacer sur la carte |
Attacker | Atk , Spd | Permet à l'entité d'attaquer d'autre entités. |
Spawner | Types , Queue | Permet à l'entité de produire des Actor |
Carrier | Size | Permet à l'entité de contenir d'autres entités (ex: transport) |
Caster | Spells[] | Permet à l'entité d'utiliser des capacités spéciales (ex: construction, bombardement, …) |
Une unité est un type d'entité possédant les archétypes Actor
, Movable
et Attacker
. Il leur est aussi possible de posséder l'archétype Caster
, Carrier
ou Spawner
.
Nom | Type | Hp | Cost | Mov | Atk | Spd |
---|---|---|---|---|---|---|
Tank | Heavy | 2 | 5 | 1 | 15 | 3 |
Soldier | Light | 1 | 2 | 1 | 5 | 1 |
Un bâtiment est un type d'acteur possédant l'archétypes Actor
. Il ne peut pas être déplacé mais peut posséder les archétypes Spawner
, Carrier
et Attacker
.
Nom | Hp. | Archétypes |
---|---|---|
Base | 20 | Spawner of [Light, Heavy], Queue: 5 |
Le jeu fonctionne selon une architecture client-serveur avec le serveur servant d’autorité pour la connexion, le matchmaking et le déroulement des parties. Les deux parties utilisent la libraire Netty pour faciliter l’échange des données sur le réseau. Une interface d'administration externe permet aux administateurs de gérer les joueurs.
Le serveur de jeu permet au client d'établir une session ce qui leur permet d'entrer dans la file d'attente pour participer à une partie de jeu. Lorsque suffisament de joueurs sont prêts, le serveur notifie les clients qu'ils sont placés dans un Lobby. Chaque joueur doit alors éventuellement sélectionner ses paramètres de jeu (future proofing). Lorsqu'ils sont prêts, les clients le signalent alors au serveur. Le Lobby possède aussi un timer interne qui empêche un client malveillant d'empêcher le départ de la partie. Une fois que tous les clients ont déclaré qu’ils sont prêts à démarrer la partie le serveur crée alors une session de jeu (partie) avec les paramètres spécifiés dans le lobby et notifie les clients du début de la partie. La création d'une session de jeu consiste à construire l’ECS représentant l’état de jeu et tirer au sort l’ordre de passage des joueurs. Lorsqu’un client demande d’effectuer une action, le serveur tente de l’appliquer à l’ECS. Si l’action est un succès tous les joueurs sont notifiés des changements à l’état de jeu, dans le cas contraire, seul le joueur ayant initié l’action est notifié.
Le client est composé d’un écran de login, d’un menu ainsi que d’une interface de jeu. Lorsqu’il est démarré, le client affiche l’interface de login, qui donne accès au Lobby sur le serveur. Une fois connecté, une interface menu permet de se déconnecter ou d’indiquer que l’on est prêt à commencer la partie. Le client entre alors en phase d’attente d’une partie de la part du serveur. Une fois la partie reçue, l’interface de jeu est alors affichée, la carte, la base et les unités éventuelles sont présentes (voir annexe 1). Chaque action faite par le client est vérifiée par le serveur. Une réponse de la part du serveur est reçue dans tous les cas (valide ou non-valide).
La base de données de l'application peut être gérée à l'aide d'une interface web par les administrateurs.
Le système utilise Netty 4.1 comme librarie réseau client-serveur qui offre une gestion robuste du pipeline d'encodage/décodage des données à travers le réseau. La librarie Google Guava est utilisée pour son mécanisme de services et son bus de message. La librarie Ashley est utilisée pour la gestion de l'état de jeu à l'aide d'un ECS.
Le client utilise libGDX comme moteur d'affichage. Cette librarie fournit de nombreuses abstractions au dessus de OpenGL qui permettent de facilement manipuler de nombreux éléments graphiques sans impact de performance.
Le tout est programmé dans le language Kotlin.
<uml title=“UseCase”>
Anonymous –> (Create Account)
Player –> (Play a Game) (Play a Game) ..> (Queue up for a Game) : <include> (Queue up for a Game) ..> (Log in to the Client) : <include>
Player <|– Administrator
Administrator –> (Ban Account) (Ban Account) ..> (Login in to the Admin)
Administrator <|– :System Administrator: :System Administrator: –> (Start Server)
</uml>
Nom | Description |
---|---|
Anonymous | Utilisateur ne possédant pas de compte |
Player | Utilisateur possédant un compte |
Administrator | Rôle permettant d’administrer le système |
Nom | Description |
---|---|
Create Account | Un utilisateur peut créer un compte |
Login to the Game | Un utilisateur peut s’authentifier auprès du serveur et récupérer une session |
Queue up for a Game | Un utilisateur peut entrer dans la file d’attente pour une partie |
Play a Game | Un utilisateur peut participer à une partie |
Login to the Admin | Un administrateur peut se connecter à l’interface d’administration |
Ban Account | Un administrateur peut bannir un joueur |
Start Server | Un administrateur système peut démarrer le serveur de jeu |
Prérequis: Le client doit être lancé
LoginSystem
)Prérequis: Le client doit être lancé, L'utilisateur doit avoir crée un compte
LoginSystem
)Prérequis: L'utilisateur doit être authentifié auprès du serveur
Prérequis: Le joueur doit être dans une partie
Prérequis: Connexion à la console d'administration
<uml title=“Authentication Packets”> hide empty methods hide empty fields
abstract class Packet {
+ id: long
}
Packet <|– ResponsePacket abstract class ResponsePacket {
+ responseTo: long
}
Packet <|– LoginRequest class LoginRequest «(P,Salmon)» {
+ username: String + password: String <i>hashed</i>
}
ResponsePacket <|– LoginResponse abstract class LoginResponse
LoginResponse <|– LoginSuccess class LoginSuccess «(P,Salmon)» {
+ sessionId: String + user: User
}
LoginResponse <|– LoginFailure class LoginFailure «(P,Salmon)» {
+ message: String
} </uml>
<uml title=“Authentication Network Exchange”> Client → Server: LoginRequest(username, password) Server –> UserManager: authenticate(username) Server ←- UserManager: «success» Server → Client: LoginSuccess(user, sessionId) alt Failure Server ←- UserManager: «failure» Server → Client: LoginFailure(reason) end </uml>
<uml title=“Authentication Workflow”> title authenticate(user) start if (user in database?) then (no)
:LoginFailure;
elseif (password matches?) then (no)
:LoginFailure;
elseif (banned?) then (yes)
:LoginFailure;
else
:create session id; :LoginSuccess;
endif stop </uml>
<uml title=“Account Creation Network Exchange”> Client → Server: CreateAccount(username, …) Server –> UserManager: createAccount(…) Server ←- UserManager: «success» Client ← Server: CreateSuccess() alt Failue Server ←- UserManager: «failure» Client ← Server: CreateFailure() end </uml>
<uml title=“Matchmaking Packets”> hide empty methods hide empty fields
enum QueueType {
1v1 2v2 3v3
}
Packet <|– EnterQueue EnterQueue – QueueType : type
Packet <|– EnterLobby class EnterLobby «(P,Salmon)» {
+ lobby: Lobby
}
Packet <|– PlayerReady class PlayerReady «(P,Salmon)» {
+ player: User
}
Packet <|– EnterGame class EnterGame «(P,Salmon)» {
+ gameId: long
} </uml>
<uml title=“Matchmaking Workflow”> actor ClientA actor ClientB box “Server Software” participant Server participant “1v1 Queue” as Queue participant “Lobby #1” as Lobby participant “Game #1” as Game end box note over Queue: is empty
ClientA → Server: EnterQueue(1v1) Server –> Queue: enterQueue(ClientA, 1v1) activate Queue
ClientB → Server: EnterQueue(1v1) Server –> Queue: enterQueue(ClientB, 1v1)
Queue –> Lobby: «new» deactivate Queue activate Lobby
ClientA ← Lobby: EnterLobby(#1) ClientB ← Lobby: EnterLobby(#1)
ClientA → Lobby: PlayerReady(ClientA) hnote over Lobby broadcast PlayerReady(ClientA) endnote
ClientB → Lobby: PlayerReady(ClientB) hnote over Lobby broadcast PlayerReady(ClientB) endnote
Lobby –> Game: «new» activate Game Lobby –> Game: start() deactivate Lobby
ClientA ← Game: EnterGame(#1) ClientB ← Game: EnterGame(#1) deactivate Game </uml>
<uml title=“Game Packets”> hide empty methods hide empty fields
abstract class Packet {
+id: long
}
Packet <|– EnterGame class EnterGame «(P,Salmon)» {
+gameId: long
}
Packet <|– EndGame class EndGame «(P,Salmon)» {
+winner: PlayerRef
}
Packet <|– MoveTurn class MoveTurn «(P,Salmon)» {
+nextPlayer: PlayerRef
}
Packet <|– SyncGameState class SyncGameState «(P,Salmon)» {
+gameWorld: GameWorld
}
Packet <|– SyncEntityState class SyncEntityState «(P,Salmon)» {
+entityId: long +components: Component[]
} </uml>
<uml title=“Game Workflow”> actor Player box “Server Software” participant GameHandler participant TurnHandler participant StateHandler participant ActionFactory participant Action participant GameWorld end box
Player ← GameHandler: EnterGame(#1)
GameHandler –> TurnHandler: initialize() activate TurnHandler GameHandler ←- TurnHandler: «done» deactivate TurnHandler GameHandler –> StateHandler: initialize() activate StateHandler StateHandler –> GameWorld: construct() activate GameWorld StateHandler ←- GameWorld: «done» deactivate GameWorld GameHandler ←- StateHandler: «done» deactivate StateHandler Player ← GameHandler: SyncGameState(world)
Player –> Player: Select Actor Player –> Player: Request Action Player → GameHandler: PerformAction(entity, action) GameHandler –> TurnHandler: isTurnOf activate TurnHandler alt Out of Turn GameHandler ←- TurnHandler: «false» Player ← GameHandler: Error(“Out of Turn”) end GameHandler ←- TurnHandler: «true» deactivate TurnHandler GameHandler –> StateHandler: performAction() activate StateHandler
StateHandler –> ActionFactory: getAction() activate ActionFactory ActionFactory –> Action: «create» activate Action StateHandler ←- ActionFactory: «return Action» deactivate ActionFactory
StateHandler –> Action: isValid? note left: Checks if the entity is able to perform the action
StateHandler –> GameWorld: beginAction() activate GameWorld #LightBlue note left: Tracks changes to entities
StateHandler –> Action: apply() activate Action #DarkSalmon Action –> GameWorld: «changes» StateHandler ←- Action: «done» deactivate Action deactivate Action
StateHandler ←- GameWorld: getChanges() deactivate GameWorld
hnote over StateHandler broadcast SyncEntityState(changes) endnote
StateHandler –> StateHandler: checkVictory()
alt Player Wins hnote over StateHandler broadcast GameEnd(victor) endnote end
GameHandler ←- StateHandler: «done» deactivate StateHandler
… Passing Turn …
Player → GameHandler: EndTurn() GameHandler –> TurnHandler: nextTurn() activate TurnHandler GameHandler ←- TurnHandler: «new player» deactivate TurnHandler hnote over GameHandler broadcast MoveTurn(new player) endnote
</uml>
<uml title=“Game Domain Model”> hide empty members
class Partie class Carte class Acteur class Bâtiment class Unité class Joueur class Equipe
Acteur <|– Bâtiment Acteur <|– Unité
Partie *– “*” Carte Partie *- “*” Equipe Equipe *– “*” Joueur
Joueur *- “*” Acteur
</uml>
<uml title=“Client Domain Model”> hide empty fields hide empty methods hide class circle hide interface circle
package “Networking” { interface NetworkService
LoginService ←- NetworkService LobbyService ←- NetworkService GameService ←- NetworkService
NetworkService <.d.> Netty
class Netty «(L,lightblue)» }
package “Display” { LoginScreen —> LoginService
LoginScreen <.r.> MenuScreen
MenuScreen —> LobbyService
MenuScreen <.r.> GameScreen
}
GameScreen –> Interactor Interactor –> GameState Interactor —> GameService
</uml>
libGDX permet la gestion des écrans (Screen
). Un écran occupe toute la surface de rendu
et permet de rapidement changer la présentation.
Chaque écran agit à la fois comme un Contrôlleur et une Vue. Il est chargé d'afficher sa Vue dans sa méthode render()
.
Les services réseau servent à abstraire l'interaction avec le réseau.
Permet d'abstraire les interactions avec l'état de jeu à travers le réseau. Il notifie des modifications effectuées à l'état aux observateurs pour qu'ils puissent les afficher.
<uml title=“Server Domain Model”> hide empty members hide empty fields hide class circle hide interface circle
package “Networking” { class Netty class NetworkHandler } class GameHandler class LoginHandler class MatchmakingHandler package “Login” { class User «Model» class Session class SessionManager «Singleton» } package “Matchmaking” { class MatchmakingManager «Service» class Queue } package “Game” { class GameSession class GameState class StateHandler class TurnHandler class ActionFactory }
SessionManager *– “*” Session Session *- User
Netty <..> NetworkHandler
note as N1 Packets are dispatched to the correct handler as defined by the user session endnote
NetworkHandler ..> N1 Session .r.> N1 N1 ..> MatchmakingHandler N1 ..> LoginHandler N1 ..> GameHandler
Session <.. LoginHandler: creates
MatchmakingHandler — MatchmakingManager
MatchmakingManager – Queue
GameHandler – GameSession
GameSession *– TurnHandler GameSession *–o StateHandler GameSession *– GameState StateHandler *– ActionFactory
</uml>
Une session est crée à l'établissement d'une connexion au serveur et persiste jusqu'à la déconnexion du client. La session permet de gérer l'état d'authentification ainsi que la cible des paquets.
Le matchmaking s'effectue en plaçant les sessions dans des files d'attente. Une fois qu'une file d'attente à atteint le nombre requis de joueurs en fonction de sa configuration. Une session de jeu est crée en y ajoutant les joueurs.
Lorsqu'une session de jeu est crée elle est initialisée avec ses paramètres de jeu (joueurs, carte, …). Elle transmet ensuite l'état de jeu initial les joueurs du début de partie et les notifie qu'elle est prête à recevoir les interactions des joueurs.
<uml title=“Data Model”> hide methods
class User «(E,lightblue)» {
+username +password <i>hashed</i> +rating
}
class Game «(E,lightblue)» {
+startedAt +length
}
class Stats «(E,lightblue» {
+isWinner +rating <i>At time of game</i> +score
}
class Role «(E,lightblue)» {
+name
}
class Ban «(E,lightblue)» {
+start +end +reason
}
Player “1” -d- “*” Role: roles Player “1” -r- “*” Ban: bans Game “*” -l- “1” Player (Game, Player) .. Stats </uml>
La base de données sert à répertorier les joueurs et garder trace des parties jouées pour déterminer le rating d'un joueur.
La trace des banissements des joueurs est aussi inscrite dans la base de données afin de leur refuser l'accès au jeu.
Entité | Description |
---|---|
User | Utilisateur de l'application, peut se connecter via le client |
Role | Permet de déterminer les actions auquels à droit un utilisateur |
Ban | Historique de banissement des joueurs |
Game | Partie joué |
Stats | Statistiques de la partie pour un joueur |
Rôle des participants au sein du groupe de développement
Role | Laureline | Yves | Samuel | Christophe |
---|---|---|---|---|
Project Manager | Primary | Assistant | ||
Software Architect | Primary | |||
Analyst | Primary | |||
Game Designer | Assistant | Primary | ||
Documentalist | Primary | Assistant | Assistant | Assistant |
Implementer | Server | Server | Client | Client |
Tester | Server | Server | Client | Client |
Le plan d'itérations considère les semaines du Jeudi au Mercredi suivant. Ceci est pour permettre d'avoir une réunion lors des séances de Laboratoire le Jeudi après-midi.
<columns 100% 50% 50%> Dates 14 Avril - 20 Avril
Charge de Travail 4h30 par personne (total 18h)
<newcolumn>
LoginScreen
, MenuScreen
, LobbyScreen
et GameScreen
.LoginScreen
NetworkHandler
(Module Partagé)LoginHandler
</columns>
<columns 100% 50% 50%> Avancement des Objectifs: 100%
Révision des Itérations: Les itérations suivantes ont été déplacées d'une semaine vers l'avant en éliminant l'itération tampon. Ceci nous calque sur le programme “normal”.
Améliorations Possibles: Optimiser la répartition des tâches entre les membres du groupe. Ceci est difficile principalement à cause du manque d'expérience avec des “gros” projets de cerains membres du groupe.
<newcolumn>
L'effort prévu ne prenait pas en compte le manque de connaissance vis à vis du language. C'était la perte de temps la plus importante, les itérations futures ne subiront normalement pas cela.
Malheureusement j'ai perdu beaucoup de temps à cause du langage et de la compréhension globale de la structure du projet. Le partage du travail ne s'est pas fait équitablement au final, ce qui est dommage car certains ont travaillé plus que d'autres. Avec la génération de la map je vais pouvoir plus m'impliquer. </columns>
<columns 100% 50% 50%> Dates 28 Avril - 4 Mai
Charge de Travail 4h30 par personne (total 18h)
Pouvoir effectuer une demande de matchmaking, entrer dans un lobby et “lancer” une partie une fois que tous les joueurs sont prêts
<newcolumn>
NetworkManager
utilisant le réseauMenuScreen
et LobbyScreen
LobbyHandler
</columns>
<columns 100% 50% 50%> Avancement des Objectifs: Client (70%), Serveur (70%), Admin (70%)
Explication du Retard: Mauvaise gestion du temps par rapport aux autre projets et laboratoires en cours.
Révision des Itérations: Les itérations ne vont pas être replanifiées le ratrd peut être rattrapé.
<newcolumn>
LobbyHandler
Les modules ont été mis en place, mais ils n'ont pas pu être testés car le client n'était pas implémenté.
</columns>
<columns 100% 50% 50%> Dates 5 Mai - 11 Mai
Charge de Travail 4h30 par personne (total 18h)
Initialiser l'état de jeu côté serveur et le transmettre au client.
<newcolumn>
</columns>
<columns 100% 50% 50%> Avancement des Objectifs: Client (0%), Serveur (0%), Admin (0%)
Explication du Retard: Mauvaise gestion du temps par rapport aux autre projets et laboratoires en cours.
Révision des Itérations: Les itérations ne vont pas être replanifiées le retard peut être rattrapé.
Révision Itération 2: Le match making a été partiellement implémenté, et présentait un bug non-directement résolvable. Il a donc été choisi que cet élément, non-indispensable au fonctionnement du jeu, serait supprimé de l'implémentation finale. Ceci permettra de rattraper le retard accumulé lors de cette itération (itération 3).
<newcolumn>
La création d'un client de débug permet de tester les changements dans le serveur sans attendre que le client fasse ces modifications ce qui permet d'accélérer le développement du serveur.
</columns>
<columns 100% 50% 50%> Dates 12 Mai - 18 Mai
Charge de Travail 6h00 par personne (total 24h)
Gérer le passage de tours entre les joueurs.
<newcolumn>
</columns>
<columns 100% 50% 50%> Avancement des Objectifs: Client (90%), Serveur (90%), Admin (100%)
Explication du Retard: Le retard se situe seulement au niveau de la gestion des tours côté client et serveur. Ce n'est pas terminé, mais fini à 80% de chaque côté.
Révision des Itérations: La gestion des tours est presque terminée et sera rattrapée durant l'Itération suivante. Il reste environ 1h de travail partagée entre chaque côté, pour terminer cela.
Avance: La gestion du déplacement des unités a été implémentée, côté client, dans cette Itération au lieu de l'itération 5, ceci pour effectuer des tests sur la gestion de la carte. Cette étape est donc presque terminée du côté client.
<newcolumn>
</columns>
<columns 100% 50% 50%> Dates 19 Mai - 25 Mai
Charge de Travail 5h00 par personne (total 20h)
<newcolumn>
</columns>
<columns 100% 50% 50%> Avancement des Objectifs: Client (40%), Serveur (50%), Admin (100%)
Révision des Itérations: La gestion du déplacement d'une unité côté client a été implémentée en partie dans l'itération 4.
Explication du retard:
Améliorations Possibles: Prévoir ce que les tâches (transfert, etc.) vont devoir utiliser en background, le serializer par exemple.
<newcolumn>
La transmission de l'état de jeu nécéssite l'implémentation d'une serialisation personnalisée pour pouvoir effectuer les synchronisations différentielles.
L'implémentation malheureusement pris plus de temps que prévu et n'est toujours pas terminé. Le retard est du à plusieurs tentatives de solutions qui n'ont pas abouti.
</columns>
<columns 100% 50% 50%> Dates 26 Mai - 01 Juin
Charge de Travail 5h par personne (total 20h)
Gestion des actions (dépendantes des archétypes) des acteurs
<newcolumn>
</columns>
<columns 100% 50% 50%> Avancement des Objectifs: Client (10%), Serveur (10%)
Révision des Itérations: La synchronization de l'état de jeu avec la gui (Interactor) et le serializer ont en partie été implémentés durant l'itération précédente, mais n'ont pas été terminés. Ils sont donc à terminer dans cette itération-ci.
Cette itération a été complètement déplacée dans l'itération 7. Ceci est dû à une mauvaise gestion du temps du Projet de semestre (PRO) qui nous a pris énormément de temps cette dernière semaine afin de le terminer. Ce projet a aussi ces propres problèmes poru la gestion du temps, laissant cette itération-ci impossible à réaliser.
Améliorations Possibles: Eviter de prévoir des charges de travail trop grandes lors de la finalisation de projets concurrents.
<newcolumn>
</columns>
<columns 100% 50% 50%>
Dates 02 Juin - 08 Juin
Charge de Travail 8h00 par personne (total 32h)
Objectifs
<newcolumn>
</columns>
<columns 100% 50% 50%> Avancement des Objectifs: Client (90%), Serveur (90%)
Révision des Itérations: L'itération 6 a été complètement déplacée dans cette itération. (voir bilan itération 6 pour plus de précision)
L'implémentation des “actions” d'unités est à compléter sur le serveur (créer, attaquer). L'implémentation des “actions” d'unités est à compléter sur le client (créer, attaquer, bouger). Déplacement dans l'itération 8.
Améliorations Possibles:
<newcolumn>
</columns>
<columns 100% 50% 50%>
Dates 09 Juin - 15 Juin
Charge de Travail 5h00 par personne (total 20h)
Objectifs
<newcolumn>
</columns>
<columns 100% 50% 50%> Avancement des Objectifs: Client (???%), Serveur (???%)
Révision des Itérations: Rattrapage des derniers problèmes de fonctionnement du client et serveur (itération 7).
Améliorations Possibles:
<newcolumn>
</columns>
Rendu du projet et Présentation.