Here is a tutorial on how to architect a small real-time strategy game (RTS), covering the game loop, the representation of various elements, their interactions, and task management.
The game loop is the heart of any game. It is an infinite loop that constantly executes the same steps:
Here is an example of a basic structure for the 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()
In an RTS, the main elements to represent are:
You can create types, structures, or classes to represent each of these elements:
elem Terrain:
- grid = tile array array : (width, height)
elem Ressource:
- type
- position
elem Building:
- type
- position
- owner
elem Unite:
- type
- position
- owner
- tasks
In the update_game function, you will need to apply the game rules and update
the state of each element. Here are some examples of what you might do:
fun update_game():
# Update units
for unite in unites:
unite.update()
# Update buildings
for building in buildings:
can_produce = handle_production(building)
if can_produce:
new_unit = produce_unit(building)
units.append(new_unit)
# Handle combat
for unit1 in units:
for unit2 in units:
if unit1.owner != unit2.owner:
handle_combat(unit1, unit2)
In the draw_game function, you will need to display all game elements on the
screen. Depending on the graphics library you are using, you will have
different functions to draw shapes, sprites, text, etc.
# Display the game
fun draw_game():
# Draw the terrain
for line in terrain.grid:
for tile in line:
draw_tile(tile)
# Draw the resources
for resource in resources:
draw_resource(resource)
# Draw the buildings
for building in buildings:
draw_building(building)
# Draw the units
for unit in units:
draw_unit(unit)
Functions like handle_target, handle_production, and handle_combat should
implement the game rules to manage interactions between different elements.
For example, handle_target could check if a worker unit is near a resource to
collect it, or if a combat unit is near an enemy unit to attack it.
fun handle_target(unite, target):
if is_instance(target, Ressource):
collect_ressource(unite, target)
elseif is_instance(target, Unite):
attack_unite(unite, target)
This architecture provides you with a solid foundation for developing a small RTS game. Of course, you will need to implement all the specific details of your game, such as different types of units, buildings, resources, special abilities, etc. However, by following this structure, you can easily organize your code and manage the various aspects of the game.
To integrate a task assignment and execution system into this RTS game architecture, you can add a task queue to each building and unit. Here's how you can proceed:
Example of task management in pseudo-code:
elem Task:
- type # For example: "Go-to", "Attack", "Build", "Harvest"
- target # The target object of the task (a position, a unit, a resource, etc.)
elem Building():
- type
- owner
- position
- task_queue = []
elem Unite():
- type
- owner
- position
- target = None
- task_queue = []
fun assign_task(element, task):
element.task_queue.append(task)
update_game function to execute tasksfun update_game():
# Update units
for unite in unites:
if not_empty(unite.file_tasks):
task = take_first(unite.file_tasks)
execute_task(unite, task):
# Update buildings
for building in buildings:
if not_empty(building.file_tasks):
task = take_first(building.file_tasks)
execute_task(building, task):
execute_task functionThis function should execute appropriate actions based on the task type and target object.
fun execute_task(element, task):
if task.type == "Go-to":
element.deplacer_vers(task.target)
elif task.type == "Attack":
attaquer(element, task.target)
elif task.type == "Build":
build_building(element, task.target)
elif task.type == "Harvest":
harvest_resource(element, task.target)
You can now assign tasks to buildings and units using the assign_task
function. For example, when a player clicks on a unit and a position on the
map, you can create a new task of type "Go-to" with the target position and
assign it to the selected unit.
fun traiter_entrees():
# ...
if evenement.type == LEFT_CLIC:
unite_selectionnee = get_unite_sous_souris()
position_target = evenement.position
task_deplacement = Task("Go-to", position_target)
assign_task(unite_selected, task_deplacement)
This structure allows buildings and units to manage multiple sequential actions efficiently by placing them in their task queue. It enhances the game's capability to handle complex tasks and provides greater flexibility in gameplay mechanics.
Here is a simple and minimalist complete example of an RTS game with OCaml. It includes a single type of unit, a single type of building, a single type of resource, and all elements are displayed with colored squares.
Definition of 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_owner: int;
}
type unite = {
u_pos: int * int;
u_kind: string;
u_owner: int;
}
And the corresponding initialisation functions:
(* 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;
}
This is usually a good idea to group all the elements of the game into a single structure, the game-state:
(* 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
()
;;
In this example, we have a single unit, a single type of building, and a single type of resource.
All elements (unit, building, resource) are displayed as colored squares.
Although this example is very simple, it illustrates the key concepts of RTS game architecture, such as task management, unit movement, and displaying game elements. You can use it as a foundation to add more features, such as resource management, building construction, unit combat, and more.
End of Part One.
...
Next, you can add a u_tasks field to the unit type and assign a "go-to" task
to make a "worker" unit go to a specific location.
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)
You can perform the tasks "harvest" and "build" on the same model. Upon arrival
at the destination, the unit can take the resource present at that location
(for example, by transferring the resource to a u_carry: resource list field
of the unit) or by adding a building at that location.
let b = new_building pos b_kind owner in
{ state with buildings = b :: state.buildings }
Created with Claude-3-Sonnet, 2024-08-01