Lire en français

RTS Tutorial

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.

Defining the Game Loop

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()

Representation of Elements

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


Updating the Game State

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)


Rendering Graphics

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)

Managing Interactions

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.


Task Management

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:

  1. Define a Task class
elem Task:
    - type    # For example: "Go-to", "Attack", "Build", "Harvest"
    - target  # The target object of the task (a position, a unit, a resource, etc.)

  1. Add a task queue to buildings and units
elem Building():
    - type
    - owner
    - position
    - task_queue = []

elem Unite():
    - type
    - owner
    - position
    - target = None
    - task_queue = []

  1. Add a function to assign new tasks

fun assign_task(element, task):
    element.task_queue.append(task)

  1. Update the update_game function to execute tasks
fun 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):

Implement the execute_task function

This 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)

Assigning Tasks to Buildings and Units

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.


Example

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.


 ...


Going further

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