First-Person Shooter Tutorial
In this tutorial, we'll create a first-person shooter game.
Before we begin, make sure you know how to create projects and run the game and the editor. Read this chapter first and let's start by creating a new project by executing the following command in some directory:
fyrox-template init --name=fps --style=3d
This command will create a new cargo workspace with a few projects inside, we're interested only in game
folder
in this tutorial.
fps
├───data
├───editor
│ └───src
├───executor
│ └───src
├───executor-android
│ └───src
├───executor-wasm
│ └───src
└───game
└───src
Player Prefab
Let's start by creating a prefab for the player. First-person shooters use quite simple layouts for characters - usually, it is just a physical capsule with a camera on top of it. Run the editor using the following command:
cargo run --package editor
By default, scene.rgs
scene is loaded, and it is our main scene, but for our player prefab we need a separate scene.
Go to File
menu and click New Scene
. Save the scene in data/player
folder as player.rgs
.
Great, now we are ready to create the prefab. Right-click on the __ROOT__
node in the World Viewer and find Replace Node
and select Physics -> Rigid Body
there. By doing this, we've replaced the root node of the scene to be a rigid body.
This is needed because our player will be moving.
Select rigid body and set the X/Y/Z Rotation Locked
properties to true
, Can Sleep
- to false
. The first three
properties prevents the rigid body from any undesired rotations and the last one prevents the rigid body from being excluded
from simulations.
As you may notice, the editor added a small "warning" icon near the root node - it tells us that the rigid body does not have a collider. Let's fix that:
By default, the editor creates a cube collider, but we need a capsule. Let's change that in the Inspector:
Now let's change the size of the collider, because default values are disproportional for a humanoid character:
This way the capsule is thinner and taller, which roughly corresponds to a 1.8m tall person. Now we need to add a camera, because without it, we couldn't see anything.
Put the camera at the top of the capsule like so:
Awesome, at this point we're almost done with this prefab. Save the scene (File -> Save Scene
) and let's start writing
some code.
Code
Now we can start writing some code, that will drive our character. Game logic is located in scripts.
Navigate to the fps
directory and execute the following command there:
fyrox-template script --name=player
This command creates a new script for our player in game/src
folder. Next, Replace the imports in the lib.rs
with the ones below:
#![allow(unused)] fn main() { use crate::{player::Player}; use fyrox::{ core::pool::Handle, core::{reflect::prelude::*, visitor::prelude::*}, plugin::{Plugin, PluginContext, PluginRegistrationContext}, scene::Scene, event::Event, gui::message::UiMessage, }; use std::path::Path; }
and then add the new module to the lib.rs
module by adding the pub mod player;
after the imports:
#![allow(unused)] fn main() { // Add this line pub mod player; }
All scripts must be registered in the engine explicitly, otherwise they won't work. To do that, add the following
lines to the register
method:
#![allow(unused)] fn main() { context .serialization_context .script_constructors .add::<Player>("Player"); }
Great, now the new script is registered, we can head over to the player.rs
module and start writing a basic character controller.
First replace the import to the following:
#![allow(unused)] fn main() { use fyrox::graph::SceneGraph; use fyrox::{ core::{ algebra::{UnitQuaternion, UnitVector3, Vector3}, pool::Handle, reflect::prelude::*, type_traits::prelude::*, variable::InheritableVariable, visitor::prelude::*, }, event::{DeviceEvent, ElementState, Event, MouseButton, WindowEvent}, keyboard::{KeyCode, PhysicalKey}, scene::{node::Node, rigidbody::RigidBody}, script::{ScriptContext, ScriptTrait, ScriptDeinitContext}, }; }
Let's start by input
handling. At first, add the following fields to the Player
struct:
#![allow(unused)] fn main() { #[visit(optional)] #[reflect(hidden)] move_forward: bool, #[visit(optional)] #[reflect(hidden)] move_backward: bool, #[visit(optional)] #[reflect(hidden)] move_left: bool, #[visit(optional)] #[reflect(hidden)] move_right: bool, #[visit(optional)] #[reflect(hidden)] yaw: f32, #[visit(optional)] #[reflect(hidden)] pitch: f32, }
The first four fields are responsible for movement in four directions and the last two responsible for camera rotation.
The next thing that we need to do is properly react to incoming OS events to modify the variables that we've just
defined. Add the following code to the on_os_event
method like so:
#![allow(unused)] fn main() { fn on_os_event(&mut self, event: &Event<()>, _ctx: &mut ScriptContext) { match event { // Raw mouse input is responsible for camera rotation. Event::DeviceEvent { event: DeviceEvent::MouseMotion { delta: (dx, dy), .. }, .. } => { // Pitch is responsible for vertical camera rotation. It has -89.9..89.0 degree limits, // to prevent infinite rotation. let mouse_speed = 0.35; self.pitch = (self.pitch + *dy as f32 * mouse_speed).clamp(-89.9, 89.9); self.yaw -= *dx as f32 * mouse_speed; } // Keyboard input is responsible for player's movement. Event::WindowEvent { event: WindowEvent::KeyboardInput { event, .. }, .. } => { if let PhysicalKey::Code(code) = event.physical_key { let is_pressed = event.state == ElementState::Pressed; match code { KeyCode::KeyW => { self.move_forward = is_pressed; } KeyCode::KeyS => { self.move_backward = is_pressed; } KeyCode::KeyA => { self.move_left = is_pressed; } KeyCode::KeyD => { self.move_right = is_pressed; } _ => (), } } } _ => {} } // ... }
This code consists from two major parts:
- Raw mouse input handling for camera rotations: we're using horizontal movement to rotate the camera around vertical axis and vertical mouse movement is used to rotate the camera around horizontal axis.
- Keyboard input handling for movement.
This just modifies the internal script variables, and basically does not affect anything else.
Now let's add camera rotation, at first we need to know the camera handle. Add the following field to the Player
struct:
#![allow(unused)] fn main() { #[visit(optional)] camera: Handle<Node>, }
We'll assign this field later in the editor, for let's focus on the code. Add the following piece of code at the start of
the on_update
:
#![allow(unused)] fn main() { let mut look_vector = Vector3::default(); let mut side_vector = Vector3::default(); if let Some(camera) = ctx.scene.graph.try_get_mut(self.camera) { look_vector = camera.look_vector(); side_vector = camera.side_vector(); let yaw = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), self.yaw.to_radians()); let transform = camera.local_transform_mut(); transform.set_rotation( UnitQuaternion::from_axis_angle( &UnitVector3::new_normalize(yaw * Vector3::x()), self.pitch.to_radians(), ) * yaw, ); } }
This piece of code is relatively straightforward: at first we're trying to borrow the camera in the scene graph using its handle, if it is succeeded, we form two quaternions that represent rotations around Y and X axes and combine them using simple multiplication.
Next thing we'll add movement code. Add the following code to the end of on_update
:
#![allow(unused)] fn main() { fn on_update(&mut self, ctx: &mut ScriptContext) { // Borrow the node to which this script is assigned to. We also check if the node is RigidBody. if let Some(rigid_body) = ctx.scene.graph.try_get_mut_of_type::<RigidBody>(ctx.handle) { // Form a new velocity vector that corresponds to the pressed buttons. let mut velocity = Vector3::new(0.0, 0.0, 0.0); if self.move_forward { velocity += look_vector; } if self.move_backward { velocity -= look_vector; } if self.move_left { velocity += side_vector; } if self.move_right { velocity -= side_vector; } let y_vel = rigid_body.lin_vel().y; if let Some(normalized_velocity) = velocity.try_normalize(f32::EPSILON) { let movement_speed = 240.0 * ctx.dt; rigid_body.set_lin_vel(Vector3::new( normalized_velocity.x * movement_speed, y_vel, normalized_velocity.z * movement_speed, )); } else { // Hold player in-place in XZ plane when no button is pressed. rigid_body.set_lin_vel(Vector3::new(0.0, y_vel, 0.0)); } } } }
This code is responsible for movement when any of WSAD keys are pressed. At first, it tries to borrow the node to which this script is assigned to, then it checks if any of the WSAD keys are pressed, and it forms a new velocity vector using the basis vectors of node. As the last step, it normalizes the vector (makes it unity length) and sets it to the rigid body velocity.
Our script is almost ready, now all we need to do is to assign it to the player's prefab. Open the player.rgs
prefab
in the editor, select Player
node and assign the Player script to it. Do not forget to set Camera handle (by clicking
on the small green button and selecting Camera from the list):
Great, now we're done with the player movement. We can test it our main scene, but at first let's create a simple level.
Open scene.rgs
and create a rigid body with a collider. Add a cube as a child of the rigid body and squash it to some
floor-like shape. Select the collider and set its Shape
to Trimesh
, add a geometry source there and point it to the
floor. Select the rigid body and set its type to Static
. You can also add some texture to the cube to make it look
much better.
Now we can instantiate our player prefab in the scene. To do that, find the player.rgs
in the Asset Browser, click
on it, hold the button, move the mouse over the scene and release the button. After that the prefab should be instantiated
at the cursor position like so:
After that you can click Play
button (green triangle above the scene preview) and you should see something like this:
It should be possible to walk using WSAD keys and rotate the camera using mouse.
Conclusion
In this tutorial we've created a basic character controller, that allows you to move using keyboard and look around using mouse. This tutorial showed the main development strategies used in the engine, that should help you to build your own game. In the next tutorial we'll add weapons.