FPS Tutorial Part 1 - Character Controller.
WARNING: This tutorial is using obsolete engine features, which are subject to be removed in future versions!
Source code: GitHub
Table of contents
- Introduction
- Creating a window
- Creating your first scene
- Using the scene
- Character controller
- Finishing touch
- Conclusion
Introduction
Fyrox is a general purpose 3D engine, it allows creating any kind of 3D game, but today we'll focus on classic 3D shooter. In this tutorial we'll write a simple character controller. This is what we're aiming for:
Let's start by creating a new cargo project, make a folder and execute this:
cargo init --bin
Open Cargo.toml and add fyrox
dependency:
[dependencies]
fyrox = "0.28.0"
Creating a window
Great! Now we can start writing the game. Let's start from something very simple - a window and a main loop. Just copy
and paste this code in the main.rs
:
extern crate fyrox; use fyrox::{ core::{ algebra::{UnitQuaternion, Vector3}, pool::Handle, }, engine::{resource_manager::ResourceManager, Engine, EngineInitParams, SerializationContext}, event::{DeviceEvent, ElementState, Event, VirtualKeyCode, WindowEvent}, event_loop::{ControlFlow, EventLoop}, resource::texture::TextureWrapMode, scene::{ base::BaseBuilder, camera::{CameraBuilder, SkyBox, SkyBoxBuilder}, collider::{ColliderBuilder, ColliderShape}, node::Node, rigidbody::RigidBodyBuilder, transform::TransformBuilder, Scene, }, window::WindowBuilder, }; use std::{sync::Arc, time}; use fyrox::window::WindowAttributes; use fyrox::engine::{GraphicsContextParams, GraphicsContext}; // Our game logic will be updated at 60 Hz rate. const TIMESTEP: f32 = 1.0 / 60.0; struct Game { // Empty for now. } impl Game { pub fn new() -> Self { Self {} } pub fn update(&mut self) { // Game logic will be placed here. } } fn main() { // Create event loop that will be used to "listen" events from the OS. let event_loop = EventLoop::new(); // Finally create an instance of the engine. let graphics_context_params = GraphicsContextParams { window_attributes: WindowAttributes { title: "3D Shooter Tutorial".to_string(), resizable: true, ..Default::default() }, vsync: true, }; let serialization_context = Arc::new(SerializationContext::new()); let mut engine = Engine::new(EngineInitParams { graphics_context_params, resource_manager: ResourceManager::new(serialization_context.clone()), serialization_context, }) .unwrap(); // Initialize game instance. It is empty for now. let mut game = Game::new(); // Run the event loop of the main window. which will respond to OS and window events and update // engine's state accordingly. Engine lets you to decide which event should be handled, // this is a minimal working example of how it should be. let mut previous = time::Instant::now(); let mut lag = 0.0; event_loop.run(move |event, _, control_flow| { match event { Event::MainEventsCleared => { // This main game loop - it has fixed time step which means that game // code will run at fixed speed even if renderer can't give you desired // 60 fps. let elapsed = previous.elapsed(); previous = time::Instant::now(); lag += elapsed.as_secs_f32(); while lag >= TIMESTEP { lag -= TIMESTEP; // Run our game's logic. game.update(); // Update engine each frame. engine.update(TIMESTEP, control_flow, &mut lag, Default::default()); } // Rendering must be explicitly requested and handled after RedrawRequested event is received. if let GraphicsContext::Initialized(ref ctx) = engine.graphics_context { ctx.window.request_redraw(); } } Event::RedrawRequested(_) => { // Render at max speed - it is not tied to the game code. engine.render().unwrap(); } Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::KeyboardInput { input, .. } => { // Exit game by hitting Escape. if let Some(VirtualKeyCode::Escape) = input.virtual_keycode { *control_flow = ControlFlow::Exit } } WindowEvent::Resized(size) => { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. engine.set_frame_size(size.into()).unwrap(); } _ => (), }, _ => *control_flow = ControlFlow::Poll, } }); }
Wow! There is lots of code for such a simple task. Fear not, everything here is pretty straightforward, let's dive into this code and disassemble it line by line. Just skip imports, it's too boring. Let's look at this line:
#![allow(unused)] fn main() { const TIMESTEP: f32 = 1.0 / 60.0; }
Here we define a rate of update for logic of our future game, just sticking to common 60 FPS. Next goes the skeleton of the game, just a struct with two methods. It will be filled later in this tutorial.
#![allow(unused)] fn main() { struct Game { // Empty for now. } impl Game { pub fn new() -> Self { Self {} } pub fn update(&mut self) { // Game logic will be placed here. } } }
Finally, we at the point where the interesting stuff happens - fn main()
. We're starting by creating our event loop:
#![allow(unused)] fn main() { extern crate fyrox; use fyrox::event_loop::EventLoop; let event_loop = EventLoop::new(); }
The event loop is a "magic" thing that receives events from the operating system and feeds your application, this is a very important part which makes the application work. Finally, we're creating an instance of the engine:
#![allow(unused)] fn main() { let graphics_context_params = GraphicsContextParams { window_attributes: WindowAttributes { title: "3D Shooter Tutorial".to_string(), resizable: true, ..Default::default() }, vsync: true, }; let serialization_context = Arc::new(SerializationContext::new()); let mut engine = Engine::new(EngineInitParams { graphics_context_params, resource_manager: ResourceManager::new(serialization_context.clone()), serialization_context, }) .unwrap(); }
At first, we're creating an instance of SerializationContext
- it is used to store type constructors used for
serialization needs. Next, we're filling EngineInitParams
structure, there is nothing interesting there, except maybe
a flag that is responsible for vertical synchronization (VSync). In this tutorial we'll have VSync disabled, because
it requires specific platform-dependent extensions which are not always available and calling .unwrap()
might result
in panic on some platforms. Next we're creating an instance of the game, remember this line, it will be changed soon:
#![allow(unused)] fn main() { let mut game = Game::new(); }
Next we define two variables for the game loop:
#![allow(unused)] fn main() { let clock = time::Instant::now(); let mut elapsed_time = 0.0; }
At first, we "remember" the starting point of the game in time. The next variable is used to control the game loop. Finally, we run the event loop and start checking for events coming from the OS:
#![allow(unused)] fn main() { event_loop.run(move |event, _, control_flow| { match event { ... } }); }
Let's look at each event separately starting from Event::MainEventsCleared
:
#![allow(unused)] fn main() { Event::MainEventsCleared => { // This main game loop - it has fixed time step which means that game // code will run at fixed speed even if renderer can't give you desired // 60 fps. let mut dt = clock.elapsed().as_secs_f32() - elapsed_time; while dt >= TIMESTEP { dt -= TIMESTEP; elapsed_time += TIMESTEP; // Run our game's logic. game.update(); // Update engine each frame. engine.update(TIMESTEP); } // Rendering must be explicitly requested and handled after RedrawRequested event is received. if let GraphicsContext::Initialized(ref ctx) = engine.graphics_context { ctx.window.request_redraw(); } } }
This is the heart of game loop - it stabilizes update rate of game logic by measuring time from last update call
and performs a various amount of iterations based on an amount of time since last update. This makes the game logic update
rate independent of FPS - it will be always 60 Hz for game logic even if FPS is 10. The while
loop contains
game.update()
and engine.update(TIMESTEP)
calls to update game's logic and engine internals respectively. After the
loop we're asking the engine to render the next frame. In the next match arm Event::RedrawRequested
we're handing our request:
#![allow(unused)] fn main() { Event::RedrawRequested(_) => { // Render at max speed - it is not tied to the game code. engine.render().unwrap(); } }
As you can see rendering happens in a single line of code. Next we need to handle window events:
#![allow(unused)] fn main() { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::KeyboardInput { input, .. } => { // Exit game by hitting Escape. if let Some(VirtualKeyCode::Escape) = input.virtual_keycode { *control_flow = ControlFlow::Exit } } WindowEvent::Resized(size) => { // It is very important to handle Resized event from window, because // renderer knows nothing about window size - it must be notified // directly when window size has changed. engine.set_frame_size(size.into()).unwrap(); } _ => (), }, }
Here we're just checking if the player has hit Escape button and exit game if so. Also, when WindowEvent::Resized
is
received, we're notifying renderer about that, so it's render targets will be resized too. The final match arm is for
every other event, nothing fancy here - just asking engine to continue listening for new events.
#![allow(unused)] fn main() { _ => *control_flow = ControlFlow::Poll, }
So far so good. This small piece of code just creates a new window and fills it with black color, now we can start writing the game.
Let's start by creating a simple scene where we'll test our character controller. This is the time when Fyroxed comes into play - Fyroxed is a native scene editor of the engine. It is worth mentioning what "scene editor" means: unlike many other engines (Unity, UnrealEngine, etc.), Fyroxed does not allow you to run your game inside it, instead you just edit your scene, save it in the editor and load it in your game. Being able to run a game inside the editor was a very huge task for one person, and I just chose the easiest way. Alright, back to the interesting stuff. Build the editor first using instructions from its GitHub page using specific commit stated in the beginning of the article.
Creating your first scene
This section is completely optional, if you eager to make the game - just use a pre-made scene (download it and unpack in the folder of your game) and go to the next section. Open Fyroxed, it should look like this:
It will ask you to choose a working directory.
The working directory is simply a path to your game's executable, in most cases it will be the root folder of your project.
Next, click File -> CreateScene
. Now you can start modifying your scene. All we need for now is a floor and maybe
some decorations. To do that, you can either create everything from simple objects (cubes, cones, cylinders,
etc.) or load some assets made in 3D editors (like Blender, 3Ds max, etc.). Here we combine two approaches: floor will
be just a squashed cube and decorations will be 3D models. Let's start from the floor. Click Create -> Mesh -> Cube
,
select the cube and use Scale tool from the toolbar to squash it to form the floor.
Next we need to add physical body to the floor to not fall through it. This is very simple, click Create -> Physics -> Rigid Body
then right-click on the rigid body in the World Viewer and click Create -> Physics -> Collider
. Next we need to bind the
floor 3D model with the rigid body, to do that drag'n'drop the floor entity to the rigid body. Now we need to configure the
collider of the rigid body. Select it and go to Inspector, find Shape property and select Trimesh from the dropdown list.
Next, click +
sign in Sources and then drag'n'drop floor entity to Unassigned
entry while holding Alt
on the keyboard.
By doing this, we've added a source of geometry for triangle mesh collider. Also, we need to make the rigid body
static, so it won't be affected by gravity and external forces, otherwise the floor will fall as any other
dynamic rigid body. To do that, simply select the body and change its Body Type
property to Static
.
Ok, good, but it looks awful, let's add some texture to it, to do that,
download floor texture, place it to data/textures
and apply it to the floor.
To do that, use the asset browser: at its left side it shows file system of your project, locate data/textures
folder
and select floor.jpg
. Now just drag-n-drop the texture to the floor, this is what you should get.
Now let's add some decorations, to do that download 3D model I prepared for
this tutorial and unpack it in data/models
. Now go to the data/models
in the asset browser and just drag-n-drop the
barrel.FBX
to the scene. Now use the Scale and Move tools to adjust scale and position of the barrel, it should look
like this:
Barrel does not have any rigid body yet, and it won't interact with world. Let's fix this. As usual, click Create -> Physics -> Rigid Body
then click on the added rigid body and add a cylinder collider by right-click on it and selecting Create -> Physics -> Colider
.
Now select the collider and set its shape to Cylinder adjust its height and radius. As a final step drag'n'drop the barrel.FBX
scene
node on the rigid body node.
Now clone some barrels, to do that select a parent rigid body of some barrel.FBX
in the World Outliner
,
right-click on the scene preview and press Ctrl+C
to copy the barrel and Ctrl+V
to paste. Repeat multiple times.
Also add a light source, to do that go to Create -> Light -> Point
and adjust its position using the Move tool.
The final step: save your scene in data/models
, to do that go to File -> Save
and select the folder and type name
of the scene in the field it should be scene.rgs
.
Using the scene
Now it's the time to load the scene we've made earlier in the game. This is very simple, all we need to do is to load
scene as resource and create its instance. Change fn new()
body to:
#![allow(unused)] fn main() { extern crate fyrox; use fyrox::{ core::{algebra::Vector3, pool::Handle}, engine::Engine, scene::{ base::BaseBuilder, camera::CameraBuilder, node::Node, transform::TransformBuilder, Scene, }, }; struct Stub { camera: Handle<Node>, scene: Handle<Scene>, } impl Stub { pub async fn new(engine: &mut Engine) -> Self { let mut scene = Scene::new(); // Load a scene resource and create its instance. engine .resource_manager .request_model("data/models/scene.rgs") .await .unwrap() .instantiate(&mut scene); // Next create a camera, it is our "eyes" in the world. // This can also be made in editor, but for educational purpose we'll made it by hand. let camera = CameraBuilder::new( BaseBuilder::new().with_local_transform( TransformBuilder::new() .with_local_position(Vector3::new(0.0, 1.0, -3.0)) .build(), ), ) .build(&mut scene.graph); Self { camera, scene: engine.scenes.add(scene), } } } }
You may have noticed that the Game
structure now has two new fields:
#![allow(unused)] fn main() { struct Game { scene: Handle<Scene>, // A handle to the scene camera: Handle<Node>, // A handle to the camera } }
These fields are just handles to the "entities" we've created in the Game::new()
. Also, change let mut game = Game::new();
to this:
#![allow(unused)] fn main() { let mut game = fyrox::core::futures::executor::block_on(Game::new(&mut engine)); }
Here we execute async function Game::new()
and it creates game's instance with the scene we've made previously.
Run the game and you should see this:
Cool! Now let's disassemble fn new()
line by line. First, we're creating an empty scene:
#![allow(unused)] fn main() { let mut scene = Scene::new(); }
The next few lines are the most interesting:
#![allow(unused)] fn main() { engine .resource_manager .request_model("data/models/scene.rgs") .await .unwrap() .instantiate(&mut scene); }
Here we're asking the resource manager to load the scene we've made previously, awaiting while it loads and then instantiating
it on the scene
. What does "instantiation" mean? In short, it means that we're creating a copy of a scene and adding the copy
to some other scene, the engine remembers connections between clones and original entities and is capable of restoring data
from resource for the instance. At this point we've successfully instantiated the scene. However, we won't see anything
yet - we need a camera:
#![allow(unused)] fn main() { let camera = CameraBuilder::new( BaseBuilder::new().with_local_transform( TransformBuilder::new() .with_local_position(Vector3::new(0.0, 1.0, -3.0)) .build(), ), ) .build(&mut scene.graph); }
Camera is our "eyes" in the world, here we're just creating a camera and moving it a bit up and back to be able to see the scene. Finally, we're adding the scene to the engine's container for scenes, and it gives us a handle to the scene. Later we'll use the handle to borrow scene and modify it.
#![allow(unused)] fn main() { Self { camera, scene: engine.scenes.add(scene), } }
Character controller
We've made a lot of things already, but still can't move in the scene. Let's fix this! We'll start writing the character controller which will allow us to walk in our scene. Let's start with a chunk of code as usual:
#![allow(unused)] fn main() { extern crate fyrox; use fyrox::{ core::{ algebra::{UnitQuaternion, Vector3}, pool::Handle, }, engine::{resource_manager::ResourceManager, Engine}, event::{DeviceEvent, ElementState, Event, VirtualKeyCode, WindowEvent}, event_loop::{ControlFlow, EventLoop}, resource::texture::TextureWrapMode, scene::{ base::BaseBuilder, camera::{CameraBuilder, SkyBox, SkyBoxBuilder}, collider::{ColliderBuilder, ColliderShape}, node::Node, rigidbody::RigidBodyBuilder, transform::TransformBuilder, Scene, }, window::WindowBuilder, }; use std::time; #[derive(Default)] struct InputController { move_forward: bool, move_backward: bool, move_left: bool, move_right: bool, pitch: f32, yaw: f32, } struct Player { camera: Handle<Node>, rigid_body: Handle<Node>, controller: InputController, } impl Player { fn new(scene: &mut Scene) -> Self { // Create rigid body with a camera, move it a bit up to "emulate" head. let camera; let rigid_body_handle = RigidBodyBuilder::new( BaseBuilder::new() .with_local_transform( TransformBuilder::new() // Offset player a bit. .with_local_position(Vector3::new(0.0, 1.0, -1.0)) .build(), ) .with_children(&[ { camera = CameraBuilder::new( BaseBuilder::new().with_local_transform( TransformBuilder::new() .with_local_position(Vector3::new(0.0, 0.25, 0.0)) .build(), ), ) .build(&mut scene.graph); camera }, // Add capsule collider for the rigid body. ColliderBuilder::new(BaseBuilder::new()) .with_shape(ColliderShape::capsule_y(0.25, 0.2)) .build(&mut scene.graph), ]), ) // We don't want the player to tilt. .with_locked_rotations(true) // We don't want the rigid body to sleep (be excluded from simulation) .with_can_sleep(false) .build(&mut scene.graph); Self { camera, rigid_body: rigid_body_handle, controller: Default::default(), } } fn update(&mut self, scene: &mut Scene) { // Set pitch for the camera. These lines responsible for up-down camera rotation. scene.graph[self.camera].local_transform_mut().set_rotation( UnitQuaternion::from_axis_angle(&Vector3::x_axis(), self.controller.pitch.to_radians()), ); // Borrow rigid body node. let body = scene.graph[self.rigid_body].as_rigid_body_mut(); // Keep only vertical velocity, and drop horizontal. let mut velocity = Vector3::new(0.0, body.lin_vel().y, 0.0); // Change the velocity depending on the keys pressed. if self.controller.move_forward { // If we moving forward then add "look" vector of the body. velocity += body.look_vector(); } if self.controller.move_backward { // If we moving backward then subtract "look" vector of the body. velocity -= body.look_vector(); } if self.controller.move_left { // If we moving left then add "side" vector of the body. velocity += body.side_vector(); } if self.controller.move_right { // If we moving right then subtract "side" vector of the body. velocity -= body.side_vector(); } // Finally new linear velocity. body.set_lin_vel(velocity); // Change the rotation of the rigid body according to current yaw. These lines responsible for // left-right rotation. body.local_transform_mut() .set_rotation(UnitQuaternion::from_axis_angle( &Vector3::y_axis(), self.controller.yaw.to_radians(), )); } fn process_input_event(&mut self, event: &Event<()>) { match event { Event::WindowEvent { event, .. } => { if let WindowEvent::KeyboardInput { input, .. } = event { if let Some(key_code) = input.virtual_keycode { match key_code { VirtualKeyCode::W => { self.controller.move_forward = input.state == ElementState::Pressed; } VirtualKeyCode::S => { self.controller.move_backward = input.state == ElementState::Pressed; } VirtualKeyCode::A => { self.controller.move_left = input.state == ElementState::Pressed; } VirtualKeyCode::D => { self.controller.move_right = input.state == ElementState::Pressed; } _ => (), } } } } Event::DeviceEvent { event, .. } => { if let DeviceEvent::MouseMotion { delta } = event { self.controller.yaw -= delta.0 as f32; self.controller.pitch = (self.controller.pitch + delta.1 as f32).clamp(-90.0, 90.0); } } _ => (), } } } }
This is all the code we need for character controller, quite a lot actually, but as usual everything here is pretty straightforward.
#![allow(unused)] fn main() { extern crate fyrox; use fyrox::core::pool::Handle; use fyrox::engine::Engine; use fyrox::scene::Scene; struct Player; impl Player { fn new(_scene: &mut Scene) -> Self { Self } fn update(&mut self, _scene: &mut Scene) {} } // Also we must change Game structure a bit too and the new() code. struct Game { scene: Handle<Scene>, player: Player, // New } impl Game { pub async fn new(engine: &mut Engine) -> Self { let mut scene = Scene::new(); // Load a scene resource and create its instance. engine .resource_manager .request_model("data/models/scene.rgs") .await .unwrap() .instantiate(&mut scene); Self { player: Player::new(&mut scene), // New scene: engine.scenes.add(scene), } } pub fn update(&mut self, engine: &mut Engine) { self.player.update(&mut engine.scenes[self.scene]); // New } } }
We've moved camera creation to Player
, because now the camera is attached to the player's body. Also, we must add this line
in the beginning of event_loop.run(...)
to let player
handle input events:
#![allow(unused)] fn main() { game.player.process_input_event(&event); }
So, let's try to understand what happens in this huge chunk of code. Let's start from the InputController
struct,
it holds the state of the input for a single frame and rotations of player "parts".
#![allow(unused)] fn main() { #[derive(Default)] struct InputController { move_forward: bool, move_backward: bool, move_left: bool, move_right: bool, pitch: f32, yaw: f32, } }
Next goes the Player::new()
function. First, we're creating a simple chain of nodes of different kinds in the
scene graph.
#![allow(unused)] fn main() { let camera; let rigid_body_handle = RigidBodyBuilder::new( BaseBuilder::new() .with_local_transform( TransformBuilder::new() // Offset player a bit. .with_local_position(Vector3::new(0.0, 1.0, -1.0)) .build(), ) .with_children(&[ { camera = CameraBuilder::new( BaseBuilder::new().with_local_transform( TransformBuilder::new() .with_local_position(Vector3::new(0.0, 0.25, 0.0)) .build(), ), ) .build(&mut scene.graph); camera }, // Add capsule collider for the rigid body. ColliderBuilder::new(BaseBuilder::new()) .with_shape(ColliderShape::capsule_y(0.25, 0.2)) .build(&mut scene.graph), ]), ) // We don't want the player to tilt. .with_locked_rotations(true) // We don't want the rigid body to sleep (be excluded from simulation) .with_can_sleep(false) .build(&mut scene.graph); }
Basically we're making something like this:
As you can see, the camera is attached to the rigid body and has a relative position of (0.0, 0.25, 0.0)
. So when we'll
move rigid body, the camera will move too (and rotate of course).
#![allow(unused)] fn main() { Self { camera, rigid_body: rigid_body_handle, controller: Default::default(), } }
Next goes the fn update(...)
function, it is responsible for movement of the player. It starts from these lines:
#![allow(unused)] fn main() { // Set pitch for the camera. These lines responsible for up-down camera rotation. scene.graph[self.camera].local_transform_mut().set_rotation( UnitQuaternion::from_axis_angle(&Vector3::x_axis(), self.controller.pitch.to_radians()), ); }
We're borrowing the camera from the graph (scene.graph[self.camera]
) and modifying its local rotation, using a
quaternion built from an axis, and an angle.
This rotates camera in vertical direction. Let's talk about borrowing in the engine. Almost every object in the
engine "lives" in generational arenas (pool in fyrox's terminology). Pool is a contiguous chunk of memory, to be
able to "reference" an object in a pool Fyrox uses handles. Almost every entity has a single owner - the engine,
so to mutate or read data from an entity your have to borrow it first, like this:
#![allow(unused)] fn main() { // Borrow rigid body node. let body = scene.graph[self.rigid_body].as_rigid_body_mut(); }
This piece of code scene.graph[self.rigid_body]
borrows rigid_body
as either mutable or shared, depending on the context (basically
it is just an implementation of Index + IndexMut traits). Once we've borrowed objects, we can modify them. As the next
step we calculate new horizontal speed for the player:
#![allow(unused)] fn main() { // Keep only vertical velocity, and drop horizontal. let mut velocity = Vector3::new(0.0, body.lin_vel().y, 0.0); // Change the velocity depending on the keys pressed. if self.controller.move_forward { // If we moving forward then add "look" vector of the body. velocity += body.look_vector(); } if self.controller.move_backward { // If we moving backward then subtract "look" vector of the body. velocity -= body.look_vector(); } if self.controller.move_left { // If we moving left then add "side" vector of the body. velocity += body.side_vector(); } if self.controller.move_right { // If we moving right then subtract "side" vector of the body. velocity -= body.side_vector(); } // Finally new linear velocity. body.set_lin_vel(velocity); }
We don't need to modify vertical speed, because it should be controlled by the physics engine. Finally, we're setting rotation of the rigid body:
#![allow(unused)] fn main() { // Change the rotation of the rigid body according to current yaw. These lines responsible for // left-right rotation. body.local_transform_mut() .set_rotation(UnitQuaternion::from_axis_angle( &Vector3::y_axis(), self.controller.yaw.to_radians(), )); }
The next piece of code is a bit boring, but still should be addressed - it is input handling. In the process_input_event
we check input events and configure input controller accordingly. Basically we're just checking if W, S, A, D keys were
pressed or released. In the MouseMotion
arm, we're modifying yaw and pitch of the controller according to mouse
velocity. Nothing fancy, except this line:
#![allow(unused)] fn main() { self.controller.pitch = (self.controller.pitch + delta.1 as f32).clamp(-90.0, 90.0); }
Here we're just restricting pitch to [-90; 90] degree range to not let flipping camera upside-down. Now let's run the game, you should see something like this and be able to walk and turn the camera.
Finishing touch
One more thing before we end the tutorial. Black "void" around us isn't nice, let's add skybox for the camera to improve
that. Skybox is a very simple effect that significantly improves scene quality. To add a skybox, add this code first
somewhere before impl Player
:
#![allow(unused)] fn main() { extern crate fyrox; use fyrox::{ engine::{ resource_manager::{ResourceManager}, }, resource::texture::TextureWrapMode, scene::{ camera::{SkyBox, SkyBoxBuilder}, }, }; async fn create_skybox(resource_manager: ResourceManager) -> SkyBox { // Load skybox textures in parallel. let (front, back, left, right, top, bottom) = fyrox::core::futures::join!( resource_manager.request_texture("data/textures/skybox/front.jpg"), resource_manager.request_texture("data/textures/skybox/back.jpg"), resource_manager.request_texture("data/textures/skybox/left.jpg"), resource_manager.request_texture("data/textures/skybox/right.jpg"), resource_manager.request_texture("data/textures/skybox/up.jpg"), resource_manager.request_texture("data/textures/skybox/down.jpg") ); // Unwrap everything. let skybox = SkyBoxBuilder { front: Some(front.unwrap()), back: Some(back.unwrap()), left: Some(left.unwrap()), right: Some(right.unwrap()), top: Some(top.unwrap()), bottom: Some(bottom.unwrap()), } .build() .unwrap(); // Set S and T coordinate wrap mode, ClampToEdge will remove any possible seams on edges // of the skybox. let skybox_texture = skybox.cubemap().unwrap(); let mut data = skybox_texture.data_ref(); data.set_s_wrap_mode(TextureWrapMode::ClampToEdge); data.set_t_wrap_mode(TextureWrapMode::ClampToEdge); skybox } }
Then modify signature of Player::new
to
#![allow(unused)] fn main() { async fn new(scene: &mut Scene, resource_manager: ResourceManager) -> Self }
We just added resource manager parameter here, and made the function async, because we'll load a bunch of textures
in the create_skybox
function. Add following line at camera builder (before .build
):
#![allow(unused)] fn main() { .with_skybox(create_skybox(resource_manager).await) }
Also modify player creation in Game::new
to this
#![allow(unused)] fn main() { player: Player::new(&mut scene, engine.resource_manager.clone()).await, }
Next, download skybox textures from here and extract the archive in
data/textures
(all textures from the archive must be in data/textures/skybox
). Now you can run the game, and you
should see something like this:
This was the last step of this tutorial.
Conclusion
In this tutorial we've learned how to use the engine and the editor. Created simple character controller and walked on the scene we've made in the editor. I hope you liked this tutorial, and if so, please consider supporting the project on Patreon or LiberaPay. Source code is available on GitHub. In the next tutorial we'll start adding weapons.