Voici un tutoriel sur la façon d'architecturer un mini-jeu de type RTS (real-time strategy game) en abordant la game loop, la représentation des différents éléments, leurs interactions et la gestion des tâches.
La game-loop (boucle de jeu) est le coeur de tout jeu. C'est une boucle infinie qui exécute constamment les mêmes étapes :
Voici un exemple de structure de base pour la game loop :
# Initialisation du jeu
init_game()
while True do
# Traiter les entrées de l'utilisateur
get_events()
# Mettre à jour l'état du jeu
update_game()
# Afficher les graphismes
display()
Dans un RTS, les principaux éléments à représenter sont :
Vous pouvez créer des types, des structures ou des classes pour représenter chacun de ces éléments :
elem Terrain:
- grid = tile array array : (width, height)
elem Ressource:
- type
- position
elem Building:
- type
- position
- owner
elem Unite:
- type
- position
- owner
- tasks
Dans la fonction update_game, vous devrez appliquer les règles du jeu et
mettre à jour l'état de chaque élément. Voici quelques exemples de ce que vous
pourriez faire :
fun update_game():
# Mettre à jour les unités
for unite in unites:
unite.update()
# Mettre à jour les bâtiments
for batiment in batiments:
peut_produire = gerer_production(batiment)
if peut_produire:
nouvelle_unite = produire_unite(batiment)
unites.append(nouvelle_unite)
# Gérer les combats
for unite1 in unites:
for unite2 in unites:
if unite1.owner != unite2.owner:
gerer_combat(unite1, unite2)
Dans la fonction draw_game, vous devrez afficher tous les éléments du jeu à
l'écran. Selon la bibliothèque graphique que vous utilisez, vous aurez
différentes fonctions pour dessiner des formes, des sprites, du texte, etc.
# Afficher le jeu
fun draw_game():
# Dessiner le terrain
for line in terrain.grid:
for tile in line:
draw_tile(tile)
# Dessiner les ressources
for resource in resources:
draw_resource(resource)
# Dessiner les bâtiments
for building in buildings:
draw_building()
# Dessiner les unités
for unite in unites:
draw_unite(unite)
Les fonctions comme gerer_cible, gerer_production et gerer_combat devraient
implémenter les règles du jeu pour gérer les interactions entre les différents
éléments.
Par exemple, gerer_cible pourrait vérifier si une unité "ouvrière" est proche
d'une ressource pour la collecter, ou si une unité de combat est proche d'une
unité ennemie pour l'attaquer.
fun gerer_cible(unite, cible):
if is_instance(cible, Ressource):
collecter_ressource(unite, cible)
elseif is_instance(cible, Unite):
attaquer_unite(unite, cible)
Cette architecture vous donne une base solide pour développer un mini-jeu de type RTS. Bien sûr, vous devrez implémenter tous les détails spécifiques à votre jeu, comme les différents types d'unités, de bâtiments, de ressources, les capacités spéciales, etc. Mais en suivant cette structure, vous pourrez facilement organiser votre code et gérer les différents aspects du jeu.
Pour intégrer un système d'assignation et d'exécution de tâches dans cette architecture de jeu RTS, vous pouvez ajouter une file d'attente de tâches à chaque bâtiment et unité. Voici comment procéder :
Exemple de gestion des tâches en pseudo-code :
elem Tache:
- type # Par exemple : "Aller-à", "Attaquer", "Construire", "Récolter"
- cible # L'objet cible de la tâche (une position, une unité, une ressource, etc.)
elem Batiment():
- type
- proprietaire
- position
- file_taches = []
elem Unite():
- type
- proprietaire
- position
- cible = None
- file_taches = []
fun assigner_tache(element, tache):
element.file_taches.append(tache)
update_game pour exécuter les tâchesfun update_game():
# Mettre à jour les unités
for unite in unites:
if not_empty(unite.file_taches):
tache = take_first(unite.file_taches)
executer_tache(unite, tache):
# Mettre à jour les bâtiments
for batiment in batiments:
if not_empty(batiment.file_taches):
tache = take_first(batiment.file_taches)
executer_tache(batiment, tache):
executer_tacheCette fonction doit exécuter les actions appropriées en fonction du type de tâche et de l'objet cible.
fun executer_tache(element, tache):
if tache.type == "Aller-à":
element.deplacer_vers(tache.cible)
elif tache.type == "Attaquer":
attaquer(element, tache.cible)
elif tache.type == "Construire":
construire_batiment(element, tache.cible)
elif tache.type == "Récolter":
recolter_ressource(element, tache.cible)
Vous pouvez maintenant assigner des tâches aux bâtiments et unités en utilisant
la fonction assigner_tache. Par exemple, lorsqu'un joueur clique sur une unité
et une position sur la carte, vous pouvez créer une nouvelle tâche de type
"Aller-à" avec la position cible et l'assigner à l'unité sélectionnée.
fun traiter_entrees():
# ...
if evenement.type == CLICK_GAUCHE:
unite_selectionnee = get_unite_sous_souris()
position_cible = evenement.position
tache_deplacement = Tache("Aller-à", position_cible)
assigner_tache(unite_selectionnee, tache_deplacement)
Avec ce système de tâches, les bâtiments et les unités peuvent exécuter plusieurs actions de manière séquentielle en les plaçant dans leur file d'attente de tâches. Cela permet une meilleure gestion des actions complexes et une plus grande flexibilité dans le gameplay.
Voici un exemple simple avec OCaml. Il inclut un seul type d'unité, un seul type de bâtiment, un seul type de ressource, et tous les éléments sont affichés avec des carrés de couleurs.
Définition des types :
(* Types *)
type tile = int * int * int
type terrain = {
grid: tile array array;
dims: int * int;
}
type resource = {
r_pos: int * int;
r_kind: string;
}
type building = {
b_pos: int * int;
b_kind: int;
b_player_id: int;
}
type unite = {
u_pos: int * int;
u_kind: string;
u_player_id: int;
}
Et les fonctions d'initialisation correspondantes :
(* Init-functions *)
let new_tile x y d =
(x, y, d)
let new_terrain width height =
let grid = Array.init height (fun y -> Array.init width (fun x -> new_tile x y 0)) in
{ grid; dims = (width, height); }
let new_resource pos kind = {
r_pos = pos;
r_kind = kind;
}
let new_building pos kind owner = {
b_pos = pos;
b_kind = kind;
b_owner = owner;
}
let new_unite pos kind owner = {
u_pos = pos;
u_kind = kind;
u_owner = owner;
}
Il est généralement judicieux de regrouper tous les éléments du jeu dans une seule structure, l'état du jeu :
(* Game-state *)
type game_state = {
terrain: terrain;
resources: resource list;
buildings: building list;
unites: unite list;
}
let init_state () =
let unite1 = new_unite (10, 10) "worker" 0 in
{
terrain = new_terrain 68 44;
resources = [];
buildings = [];
unites = unite1 :: [];
}
Update functions:
(* Updates *)
let update_unite unite =
(unite)
let update_building building =
(building)
let update_state state =
let unites = List.map update_unite state.unites in
let buildings = List.map update_building state.buildings in
{ state with unites; buildings; }
Template to draw on an html5 canvas, with the ReScript compiler:
(* Canvas *)
let width, height = (680, 440)
let canvas = Canvas.getElementById Canvas.document "my_canvas"
let ctx = Canvas.getContext canvas "2d"
let bg_color = "#111"
let fill_rect color (x, y) =
Canvas.fillStyle ctx color;
Canvas.fillRect ctx x y 10 10;
()
;;
(* Draw *)
let draw_resource r =
fill_rect "#ec4" r.r_pos;
;;
let draw_building b =
fill_rect "#59e" b.b_pos;
;;
let draw_unite u =
fill_rect "#4d8" u.u_pos;
;;
(* Display *)
let display_game state =
(* Background *)
Canvas.fillStyle ctx bg_color;
Canvas.fillRect ctx 0 0 width height;
(* Draw Elements *)
List.iter draw_resource state.resources;
List.iter draw_building state.buildings;
List.iter draw_unite state.unites;
()
;;
(* Events *)
let keychange_event ev =
()
let mousechange_event ev =
()
(* Main *)
let () =
let state = ref (init_state ()) in
let animate () =
state := update_state !state;
display_game !state;
()
in
Canvas.addKeyEventListener Canvas.window "keydown" keychange_event true;
Canvas.addMouseEventListener Canvas.window "mousedown" mousechange_event true;
let _ = Canvas.setInterval animate (1000/20) in
()
;;
Dans cet exemple, nous avons une seule unité, un seul type de bâtiment, et une seule sorte de ressource.
Tous les éléments (unité, bâtiment, ressource) sont affichés comme des carrés de couleurs.
Bien que cet exemple soit très simple, il illustre les concepts clés de l'architecture d'un jeu RTS, comme la gestion des tâches, le déplacement des unités, et l'affichage des éléments de jeu. Vous pouvez l'utiliser comme base pour ajouter plus de fonctionnalités, comme la gestion des ressources, la construction de bâtiments, les combats entre unités, etc.
Fin de la première partie.
...
Vous pouvez ensuite ajouter un champ u_tasks au type unité, et assigner une
tâche "go-to" pour faire aller une unité "ouvrière" à un certain endroit.
type unite = {
u_pos: int * int;
u_kind: string;
u_owner: int;
u_tasks: (string * int * int) list;
}
let add_task task u =
{ u with u_tasks = task :: u.u_tasks }
(* Utils *)
let move_towards target (x, y) =
let (tx, ty) = target in
let dx = if x < tx then x + 1 else if x > tx then x - 1 else x in
let dy = if y < ty then y + 1 else if y > ty then y - 1 else y in
(dx, dy)
(* Tasks *)
let update_unite unite =
match unite.u_tasks with
| [] ->
add_task ("GoTo", Random.int 68, Random.int 44) unite
| ("GoTo", x, y) :: tl ->
if (x, y) = unite.u_pos
then { unite with u_tasks = tl }
else begin
let new_pos = move_towards (x, y) unite.u_pos in
{ unite with u_pos = new_pos }
end
| _ ->
(unite)
Vous pouvez réaliser les tâches "récolter" et "construire" sur le même model,
arrivée à destination l'unité pourra prendre la ressource présente à cet
endroit (par exemple en transférant la ressource dans un champ u_carry: resource list de l'unité), ou bien en ajoutant un bâtiment à cet endroit :
let b = new_building pos b_kind owner in
{ state with buildings = b :: state.buildings }
Created with Claude-3-Sonnet, 2024-08-01