Bocksdin Coding Logo Banner

Game Development with Fyrox and Rust (Pt 4: Enemy Spawner)

Today we're going to add enemies to our "Crowd Control" style game using the Fyrox game engine.
This tutorial assumes you have Rust installed already.

Currently, when we run > cargo run --package executor --release we should see the following:

Enemy Type DefinitionCopied!

There isn't much happening yet, so let's add some danger!
First, create the following files: types/mod.rs and types/enemy.rs.
Our types/mod.rs file will be a classic module setup:
pub mod enemy;
pub use enemy::Enemy;

Remember to declare it in your lib.rs file with mod types;.

Next, in types/enemy.rs, we'll follow the builder pattern for dynamically defining different enemies:
use fyrox::core::{reflect::prelude::*, visitor::prelude::*};

// Visit and Reflect are required for Fyrox structs
#[derive(Default, Visit, Reflect, Debug, Clone)]
pub struct Enemy {
    // Name of Node
    pub name: String,
    // Movement speed
    pub speed: f32,
    // Rectangle node scaling
    pub scale: f32,
    // Attack damage
    pub attack_damage: f32,
    // Attack speed
    pub attack_speed: f32,
}

impl Enemy {
    pub fn new() -> Self {
        // Build with default values
        Self {
            name: "".to_owned(),
            speed: 0.0,
            scale: 1.0,
            attack_damage: 0.0,
            attack_speed: 0.0,
        }
    }

    // Define name of Node
    pub fn with_name(mut self, name: &str) -> Self {
        self.name = name.to_owned();
        self
    }

    // Define movement speed
    pub fn with_speed(mut self, speed: f32) -> Self {
        self.speed = speed;
        self
    }

    // Define rectangle node scaling
    pub fn with_scale(mut self, scale: f32) -> Self {
        self.scale = scale;
        self
    }

    // Define attack damage
    pub fn with_attack_damage(mut self, attack_damage: f32) -> Self {
        self.attack_damage = attack_damage;
        self
    }

    // Define attack speed
    pub fn with_attack_speed(mut self, attack_speed: f32) -> Self {
        self.attack_speed = attack_speed;
        self
    }
}

With this pattern, we can define different enemies with different stats. We can even skip defining specific stats and use the default values.

File Structure CleanupCopied!

Before we continue adding Fyrox scripts, let's cleanup our file structure. Create scripts/mod.rs and move your player.rs into the new scripts folder.
Import and export the player script in scripts/mod.rs:
pub mod player;
pub use player::Player;

Remember to update your lib.rs imports accordingly.

Enemy Fyrox ScriptCopied!

Now, we'll create the Fyrox script that is attached to enemy nodes. Run > fyrox-template script --name enemy and move the generated file to the scripts folder.
Add the Enemy struct to the scripts/mod.rs file.

In our new scripts/enemy.rs file we first define our Enemy struct:
pub struct Enemy {
  // Self node handles
  handle: Handle<Node>,
  sprite: Handle<Node>,
  
  // Self properties
  name: String,
  speed: f32,
  scale: f32,
  attack_damage: f32,
  attack_speed: f32,

  // Initial spawn point
  starting_position: Vector2<f32>,

  // Timer for attacks
  attack_timer: f32,

  // Player node handles
  player_handle: Handle<Node>,
  player_collider: Handle<Node>,
}

Then we need to define our default values and implement the builder pattern:
impl Enemy {
  pub fn new() -> Self {
      Self {
          handle: Handle::NONE,
          sprite: Handle::NONE,
          name: "".to_owned(),
          speed: 0.0,
          scale: 1.0,
          attack_damage: 0.0,
          attack_speed: 0.0,
          starting_position: Vector2::new(0.0, 0.0),
          attack_timer: 0.0,
          player_handle: Handle::NONE,
          player_collider: Handle::NONE,
      }
  }

  pub fn with_name(mut self, name: &str) -> Self {
      self.name = name.to_owned();
      self
  }

  pub fn with_speed(mut self, speed: f32) -> Self {
      self.speed = speed;
      self
  }

  pub fn with_starting_position(mut self, position: Vector2<f32>) -> Self {
      self.starting_position = position;
      self
  }

  pub fn with_scale(mut self, scale: f32) -> Self {
      self.scale = scale;
      self
  }

  pub fn with_attack_damage(mut self, attack_damage: f32) -> Self {
      self.attack_damage = attack_damage;
      self
  }

  pub fn with_attack_speed(mut self, attack_speed: f32) -> Self {
      self.attack_speed = attack_speed;
      self
  }
}

To finish off the builder pattern, we need a build method. This method takes the properties we set and adds the necessary nodes to the game:
impl Enemy {
  ...

  // ScriptContext implements three lifetimes, but we don't use them here so leave them anonymous
  pub fn build(mut self, context: &mut ScriptContext<'_, '_, '_>) -> Handle<Node> {
    // Build a 2D rigid body
    RigidBodyBuilder::new(
        BaseBuilder::new()
            // Instantiate at the initial starting position and scale defined
            .with_local_transform(
                TransformBuilder::new()
                    .with_local_position(Vector3::new(
                        self.starting_position.x,
                        self.starting_position.y,
                        0.0,
                    ))
                    .with_local_scale(Vector3::new(self.scale, self.scale, 1.0))
                    .build(),
            )
            .with_children(&[
                // Add a 2D collider
                ColliderBuilder::new(BaseBuilder::new())
                    // Fit to the square based on the rigid body scale
                    .with_shape(ColliderShape::Cuboid(CuboidShape {
                        half_extents: Vector2::new(self.scale / 2., self.scale / 2.),
                    }))
                    .with_collision_groups(InteractionGroups {
                        // Assign it to the second collision membership group only
                        memberships: BitMask(0b0100_0000_0000_0000_0000_0000_0000_0000),
                        // Have it interact with all memberships except the first two
                        filter: BitMask(0b0011_1111_1111_1111_1111_1111_1111_1111),
                    })
                    .build(&mut context.scene.graph),
                // Add a 2D rectangle to display our sprite eventually
                {
                    self.sprite = RectangleBuilder::new(BaseBuilder::new())
                        .build(&mut context.scene.graph);
                    self.sprite
                },
            ])
            // Add *this* instance of the Enemy script 
            // to *this* instance of an Enemy node
            .with_script(Script::new(self)),
    )
    // Remove gravity and lock rotation
    // to ensure the node moves as we want
    .with_gravity_scale(0.)
    .with_rotation_locked(true)
    .build(&mut context.scene.graph)
  }
}

When the node is instantiated, we want to store a reference to the Player node for use later. This will be more performant than fetching the reference on each frame.
impl ScriptTrait for Enemy {
  fn on_init(&mut self, context: &mut ScriptContext) {
      // Store reference to *this* instance of the enemy node
      self.handle = context.handle;

      // Find the Player node
      match context.scene.graph.find_by_name_from_root("Player") {
          // (Handle<Node>, Node)
          Some(handle) => {
              // If found, store the handle
              self.player_handle = handle.0;

              // Find and store the Player's collider node handle
              for child in handle.1.children().iter() {
                  if let Some(_) = context.scene.graph[*child].cast::<Collider>() {
                      self.player_collider = *child;
                  }
              }
          }
          None => {}
      }
  }
  
  ...
}

Enemy Spawner System SetupCopied!

We have our enemies ready to be instantiated. Let's borrow an old trick from Ultima Online and have a dedicated node to handle enemy generation.
Run > fyrox-template script --name enemy_spawner and move the new script into the scripts folder.
We'll need a couple more constants defined in our src/constants.rs file:
// MAXIMUM AND MINIMUM X AND Y VALUES WITH OFFSET
pub const MAX_MAP_XY_WITH_OFFSET: i32 = MAX_MAP_XY + MAP_OFFSET;
pub const MIN_MAP_XY_WITH_OFFSET: i32 = MIN_MAP_XY + MAP_OFFSET;

In our new scripts/enemy_spawner.rs file, we need to import a couple things:
use super::Enemy;
use crate::constants::{MAX_MAP_XY_WITH_OFFSET, MIN_MAP_XY_WITH_OFFSET};
use crate::types;

Define our EnemySpawner properties:
pub struct EnemySpawner {
  // Number of seconds between enemy spawn
  pub spawn_rate: f32,
  pub spawn_timer: f32,

  // Radius of enemy spawn in relation to player
  pub spawn_radius: f32,

  // Enemy properties for spawning
  pub enemy: types::Enemy,

  // Player node handle
  pub player_handle: Handle<Node>,
}

Setup the builder pattern:
impl EnemySpawner {
  pub fn new() -> Self {
      Self {
          spawn_rate: 0.0,
          spawn_timer: 0.0,
          spawn_radius: 0.0,
          enemy: types::Enemy::new(),
          player_handle: Handle::NONE,
      }
  }

  pub fn with_spawn_rate(mut self, spawn_rate: f32) -> Self {
      // Offset the desired spawn rate 
      // to prevent all enemies syncing up when attacking
      self.spawn_rate = spawn_rate + 0.05;
      self
  }

  pub fn with_spawn_radius(mut self, spawn_radius: f32) -> Self {
      self.spawn_radius = spawn_radius;
      self
  }

  pub fn with_enemy(mut self, enemy: types::Enemy) -> Self {
      self.enemy = enemy;
      self
  }

  pub fn build(self, graph: &mut Graph) -> Handle<Node> {
      // Use a basic, unrendered node
      RectangleBuilder::new(
          BaseBuilder::new()
              // Attach *this* instance of the script
              // to *this* instance of the EnemySpawner node
              .with_script(Script::new(self))
              .with_visibility(false),
      )
      .build(graph)
  }
}

Like in the Enemy script, we'll grab a reference to the Player node on initialization:
impl ScriptTrait for EnemySpawner {
  fn on_init(&mut self, context: &mut ScriptContext) {
      match context.scene.graph.find_by_name_from_root("Player") {
          Some(handle) => self.player_handle = handle.0,
          None => {}
      }
  }

  ...
}

To keep our on_update method clean, let's define a method that handles enemy spawning:
impl Enemy {
  ...

  // ScriptContext implements three lifetimes, but we don't use them here so leave them anonymous
  fn spawn_enemy(&self, context: &mut ScriptContext<'_, '_, '_>) {
      let mut rng = rand::thread_rng();

      // Grab current player position
      let player_position = context.scene.graph[self.player_handle]
          .local_transform()
          .position();

      // Determine min/max x and y values based on player position and map bounds
      let min_x = f32::max(
          player_position.x - self.spawn_radius,
          MIN_MAP_XY_WITH_OFFSET as f32,
      );
      let max_x = f32::min(
          player_position.x + self.spawn_radius,
          MAX_MAP_XY_WITH_OFFSET as f32,
      );
      let min_y = f32::max(
          player_position.y - self.spawn_radius,
          MIN_MAP_XY_WITH_OFFSET as f32,
      );
      let max_y = f32::min(
          player_position.y + self.spawn_radius,
          MAX_MAP_XY_WITH_OFFSET as f32,
      );

      // Generate a random starting position within min/max x and y values
      let starting_position =
          Vector2::new(rng.gen_range(min_x..max_x), rng.gen_range(min_y..max_y));

      // Instantiate new enemy at starting position
      Enemy::new()
          .with_name(&self.enemy.name)
          .with_speed(self.enemy.speed)
          .with_starting_position(starting_position)
          .with_scale(self.enemy.scale)
          .with_attack_damage(self.enemy.attack_damage)
          .with_attack_speed(self.enemy.attack_speed)
          .build(context);
  }
}

Finally, call our spawn_enemy method in the on_update method according to the spawn_rate:
impl ScriptTrait for EnemySpawner {
  ...

  fn on_update(&mut self, context: &mut ScriptContext) {
      self.spawn_timer += context.dt;

      if self.spawn_timer >= self.spawn_rate {
          self.spawn_enemy(context);
          self.spawn_timer = 0.0;
      }
  }

  ...
}

Enemy Spawner System UseCopied!

Back in our src/lib.rs file we'll build and store the enemy spawners. First we need to import our new scripts:
use scripts::{enemy::Enemy, player::Player, enemy_spawner::EnemySpawner};

Then register the new Enemy script alongside the Player script:
impl PluginConstructor for GameConstructor {
  fn register(&self, context: PluginRegistrationContext) {
      context
          .serialization_context
          .script_constructors
          .add::<Player>("Player")
          .add::<Enemy>("Enemy");
  }
  
  ... 
}

Add vector to the Game struct to hold our enemy spawners:
pub struct Game {
  ...
  enemy_spawners: Vec<Handle<Node>>,
}

And initialize it in the new method:
impl Game {
  pub fn new(scene_path: Option<&str>, context: PluginContext) -> Self {
      ...

      Self {
          ...
          enemy_spawners: Vec::new(),
      }
  }

  ...
}

Next, we'll define a method that builds the enemy spawners. For the purpose of this tutorial we'll just build a single spawner, but feel free to add as many as you want.
impl Game {
  ...

  pub fn build_spawners(&mut self, graph: &mut Graph) {
    let basic_enemy = EnemySpawner::new()
        .with_spawn_rate(1.0)
        .with_spawn_radius(5.0)
        .with_enemy(
            types::Enemy::new()
                .with_name("Basic Enemy")
                .with_speed(1.0)
                .with_scale(0.5)
                .with_attack_damage(1.0)
                .with_attack_speed(1.0)
        )
        .build(graph);

    self.enemy_spawners.push(basic_enemy);
  }
}

Finally, call the build_spawners method after the scene has loaded:
impl Plugin for Game {
  ...

  fn on_scene_loaded(
    &mut self,
    _path: &Path,
    scene: Handle<Scene>,
    _data: &[u8],
    context: &mut PluginContext,
  ) {
      ...

      let graph: &mut Graph = &mut context.scenes[self.scene].graph;
      
      ...

      // Build Enemy Spawners
      self.build_spawners(graph);
  }
}

Running our game now with > cargo run --package executor --release we should see little squares spawning around our player:


It's pretty boring if our enemies just sit there. In the next post we'll have them continuously path towards the Player and periodically attack.


Full code can be found at https://github.com/bocksdin/blog-fyrox-game-dev-tutorial/tree/enemy-spawner .
Questions about the article: questions@bocksdincoding.com
General inquiries: contact@bocksdincoding.com