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 .