Writing a 3D Shooter using rg3d - #1 - Character Controller

This tutorial starts a series of tutorials for rg3d game engine. In this series we’ll make a 3D shooter - something similar to rusty-shooter. Also, the series should help you to learn basic principles which lies in the foundation of the engine.

rg3d version: 0.17
rusty-editor version: 0.6
Source code: GitHub

Table of contents

Introduction

rg3d 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 rg3d dependency:

[dependencies]
rg3d = "0.17.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:

use rg3d::{
    core::{
        algebra::{UnitQuaternion, Vector3},
        pool::Handle,
    },
    engine::{resource_manager::ResourceManager, Engine},
    event::{DeviceEvent, ElementState, Event, VirtualKeyCode, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    gui::node::StubNode,
    physics::{dynamics::RigidBodyBuilder, geometry::ColliderBuilder},
    resource::texture::TextureWrapMode,
    scene::{
        base::BaseBuilder,
        camera::{CameraBuilder, SkyBox},
        node::Node,
        transform::TransformBuilder,
        RigidBodyHandle, Scene,
    },
    window::WindowBuilder,
};
use std::time;

// Create our own engine type aliases. These specializations are needed, because the engine
// provides a way to extend UI with custom nodes and messages.
type GameEngine = Engine<(), StubNode>;

// 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() {
    // Configure main window first.
    let window_builder = WindowBuilder::new().with_title("3D Shooter Tutorial");
    // 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 mut engine = GameEngine::new(window_builder, &event_loop, true).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 minimal working example if how it should be.
    let clock = time::Instant::now();

    let mut elapsed_time = 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 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.
                engine.get_window().request_redraw();
            }
            Event::RedrawRequested(_) => {
                // Render at max speed - it is not tied to the game code.
                engine.render(TIMESTEP).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.renderer.set_frame_size(size.into());
                }
                _ => (),
            },
            _ => *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:

type GameEngine = Engine<(), StubNode>;

This is a type alias for the Engine structure, it requires 2 generic type parameters: one for custom UI message type and one for custom UI node. Since we’re not planning to write custom widgets for now, just insert stubs there and make the type alias to reduce noise in the code.

Next we define a rate of update for logic of our future game, just stick to common 60 FPS:

const TIMESTEP: f32 = 1.0 / 60.0;

Next goes the skeleton of the game, just a struct with two methods. It will be filled later in this tutorial.

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 a window builder:

let window_builder = WindowBuilder::new().with_title("3D Shooter Tutorial");

The builder will be used later by the engine to create a window. Next we’re creating event loop:

let event_loop = EventLoop::new();

Event loop is a “magic” thing that receives events from operating system and feeds your application, this is very important part which makes application work. Finally we creating an instance of the engine:

let mut engine = GameEngine::new(window_builder, &event_loop, true).unwrap();

First two parameters are the window builder, and the event loop, last one is a boolean flag that responsible for vertical synchronization (VSync). Next we creating an instance of the game, remember this line, it will be changed soon:

let mut game = Game::new();

Next we define two variables for the game loop:

let clock = time::Instant::now();
let mut elapsed_time = 0.0;

At first, we “remember” starting point of the game in time. Next variable is used to control game loop. Finally we run event loop and starting to check events coming from OS:

event_loop.run(move |event, _, control_flow| {
    match event {
        ...
    }
});

Let’s check each event separately starting from Event::MainEventsCleared:

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.
    engine.get_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 game logic update rate independent of FPS - it will be always 60 Hz rate 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 next frame. At next match arm Event::RedrawRequested we’re handing our request:

Event::RedrawRequested(_) => {
    // Render at max speed - it is not tied to the game code.
    engine.render(TIMESTEP).unwrap();
}

As you can see rendering happens in a single line of code. Next we need to handle window events:

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.renderer.set_frame_size(size.into());
	}
    _ => (),
},

Here we’re just checking if a 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. Final match arm is for every other events, nothing fancy here - just asking engine to continue listening for new events.

_ => *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.

Window

Let’s start by creating a simple scene where we’ll test character controller. This is the time when rusty-editor comes into play - rusty-editor is a native scene editor of the engine. It is worth to mention what “scene editor” means: unlike many other engines (Unity, UnrealEngine, etc.), rusty-editor does not allow you to run your game inside it, instead you’re just editing scene, save it and load in your game. Being able to run a game inside the editor was a very huge task for one person, and I just selected the easiest way. Alright, back to interesting stuff. Download the editor first, this tutorial uses rusty-editor v0.6. Newer versions probably will also work, but since the engine and the editor changes very rapidly most likely that future versions will be incompatible.

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 rusty-editor, it should look like this:

rusty-editor

It will ask you to choose a working directory and textures path.

configurator

The working directory is simply a path to your game, textures path is a path where all textures are located. Why do you need to specify the path to textures? The answer is simple: 3D models often linked with textures, but most 3D editors saves an absolute path to textures which isn’t portable. To solve this, the engine strips everything but file name and concatenates it with provided textures path to be able to load textures.

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.

Floor

Next we need to add physical body to the floor to not fall through floor. This is very simple, select Static rigid body in Node Properties panel, then select Trimesh collider and viola!

Floor Body

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.

Floor Texture

Now let’s add some decorations, to do that download 3D model I prepared for this tutorial and unpack barrel.FBX in data/models and all jpg files in data/textures. Now go to the data/models in the asset browser and just drag-n-drop the barrel.FBX to the scene. Use the Scale and Move tools to adjust scale and position of the barrel.

Barrel

Barrel does not have any rigid body yet and it won’t interact with world. Let’s fix this. Choose Dynamic in Body field, apply Cylinder collider and adjust its height and radius. Keep in mind that every physical body must be assigned to “root” node of an object, in case of barrel it will be barrel.FBX node. Why so? Physical bodies operate in global coordinates, and if you’ll assign a body to a child node, rg3d will apply global coordinates of the body as local coordinates of the child node. This will result in a desync of coordinates of the body and the node. This a common mistake made by editor’s users.

Barrel Body

Now clone some barrels, to do that select a 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.

Barrel

Also add a light source, to do that go to Create -> Light -> Point and adjust its position using the Move tool.

Barrel

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:

pub async fn new(engine: &mut GameEngine) -> 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_geometry(&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:

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:

let mut game = rg3d::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:

Barrel

Cool! Now let’s disassemble fn new() line by line. At first, we’re creating an empty scene

let mut scene = Scene::new();

Next few lines are the most interesting:

engine
    .resource_manager
    .request_model("data/models/scene.rgs")
    .await
    .unwrap()
    .instantiate_geometry(&mut scene);

Here we’re asking resource manager to load a scene we’ve made previously, awaiting while it loads and then instantiating it on the scene. What “instantiation” means? Shortly, it means that we’re creating a copy of a scene and add the copy to some other scene, the engine remembers connections between clones and original entities and capable to restore 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:

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 move 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.

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 from the chunk of code as usual:

#[derive(Default)]
struct InputController {
    move_forward: bool,
    move_backward: bool,
    move_left: bool,
    move_right: bool,
    pitch: f32,
    yaw: f32,
}

struct Player {
    pivot: Handle<Node>,
    camera: Handle<Node>,
    rigid_body: RigidBodyHandle,
    controller: InputController,
}

impl Player {
    fn new(scene: &mut Scene) -> Self {
        // Create a pivot and attach a camera to it, move it a bit up to "emulate" head.
        let camera;
        let pivot = BaseBuilder::new()
            .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
            }])
            .build(&mut scene.graph);

        // Create rigid body, it will be used for interaction with the world.
        let rigid_body_handle = scene.physics.add_body(
            RigidBodyBuilder::new_dynamic()
                .lock_rotations() // We don't want the player to tilt.
                .translation(0.0, 1.0, -1.0) // Offset player a bit.
                .build(),
        );

        // Add capsule collider for the rigid body.
        scene.physics.add_collider(
            ColliderBuilder::capsule_y(0.25, 0.2).build(),
            rigid_body_handle,
        );

        // Bind pivot with rigid body. Scene will automatically sync transform of the pivot
        // with the transform of the rigid body.
        scene.physics_binder.bind(pivot, rigid_body_handle);

        Self {
            pivot,
            camera,
            rigid_body: rigid_body_handle.into(),
            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 the pivot in the graph.
        let pivot = &mut scene.graph[self.pivot];

        // Borrow rigid body in the physics.
        let body = scene
            .physics
            .bodies
            .get_mut(self.rigid_body.into())
            .unwrap();

        // Keep only vertical velocity, and drop horizontal.
        let mut velocity = Vector3::new(0.0, body.linvel().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 pivot.
            velocity += pivot.look_vector();
        }
        if self.controller.move_backward {
            // If we moving backward then subtract "look" vector of the pivot.
            velocity -= pivot.look_vector();
        }
        if self.controller.move_left {
            // If we moving left then add "side" vector of the pivot.
            velocity += pivot.side_vector();
        }
        if self.controller.move_right {
            // If we moving right then subtract "side" vector of the pivot.
            velocity -= pivot.side_vector();
        }

        // Finally new linear velocity.
        body.set_linvel(velocity, true);

        // Change the rotation of the rigid body according to current yaw. These lines responsible for
        // left-right rotation.
        let mut position = *body.position();
        position.rotation =
            UnitQuaternion::from_axis_angle(&Vector3::y_axis(), self.controller.yaw.to_radians());
        body.set_position(position, true);
    }

    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.

// 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 GameEngine) -> 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_geometry(&mut scene);

        Self {
            player: Player::new(&mut scene), // New
            scene: engine.scenes.add(scene),
        }
    }

    pub fn update(&mut self, engine: &mut GameEngine) {
        self.player.update(&mut engine.scenes[self.scene]); // New
    }
}

We’ve moved camera creation to Player, because now camera is attached to player’s body. Also, we must add this line in the beginning of event_loop.run(...) to let player handle input events:

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”.

#[derive(Default)]
struct InputController {
    move_forward: bool,
    move_backward: bool,
    move_left: bool,
    move_right: bool,
    pitch: f32,
    yaw: f32,
}

Next goes the the Player::new() function. First, we’re creating simple chain of nodes of different kinds in the scene graph.

let camera;
let pivot = BaseBuilder::new()
    .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
    }])
    .build(&mut scene.graph);

Basically we’re making something like this:

Graph

As you can see, the camera is attached to the pivot and has relative position of (0.0, 0.25, 0.0). So when we’ll move pivot, the camera will move too (and rotate of course). Next we’re adding a rigid body with a capsule collider and link it with the pivot.

// Create rigid body, it will be used for interaction with the world.
let rigid_body_handle = scene.physics.add_body(
    RigidBodyBuilder::new_dynamic()
        .lock_rotations() // We don't want the player to tilt.
        .translation(0.0, 1.0, -1.0) // Offset player a bit.
        .build(),
);

// Add capsule collider for the rigid body.
scene.physics.add_collider(
    ColliderBuilder::capsule_y(0.25, 0.2).build(),
    rigid_body_handle,
);

// Bind pivot with rigid body. Scene will automatically sync transform of the pivot
// with the transform of the rigid body.
scene.physics_binder.bind(pivot, rigid_body_handle);

Comments should clarify what is going on here. Finally, we’re creating Player instance and return it:

Self {
    pivot,
    camera,
    rigid_body: rigid_body_handle.into(),
    controller: Default::default(),
}

Next goes the fn update(...) function, it is responsible for movement of the player. It starts from these lines:

// 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 in the graph (scene.graph[self.camera]) and modify 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 rg3d’s terminology). Pool is a contiguous chunk of memory, to be able to “reference” an object in a pool rg3d uses handles. Almost every engine’s entity has single owner - the engine, so to mutate or read a data from an entity your have to borrow it first, like this:

// Borrow the pivot in the graph.
let pivot = &mut scene.graph[self.pivot];

// Borrow rigid body in the physics.
let body = scene
    .physics
    .bodies
    .get_mut(self.rigid_body.into())
    .unwrap();

This piece of code scene.graph[self.pivot] borrows pivot 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:

// Keep only vertical velocity, and drop horizontal.
let mut velocity = Vector3::new(0.0, body.linvel().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 pivot.
    velocity += pivot.look_vector();
}
if self.controller.move_backward {
    // If we moving backward then subtract "look" vector of the pivot.
    velocity -= pivot.look_vector();
}
if self.controller.move_left {
    // If we moving left then add "side" vector of the pivot.
    velocity += pivot.side_vector();
}
if self.controller.move_right {
    // If we moving right then subtract "side" vector of the pivot.
    velocity -= pivot.side_vector();
}

// Finally new linear velocity.
body.set_linvel(velocity, true);

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:

// Change the rotation of the rigid body according to current yaw. These lines responsible for
// left-right rotation.
let mut position = *body.position();
position.rotation = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), self.controller.yaw.to_radians());
body.set_position(position, true);

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:

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.

Controller

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:

async fn create_skybox(resource_manager: ResourceManager) -> SkyBox {
    // Load skybox textures in parallel.
    let (front, back, left, right, top, bottom) = rg3d::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 = SkyBox {
        front: Some(front.unwrap()),
        back: Some(back.unwrap()),
        left: Some(left.unwrap()),
        right: Some(right.unwrap()),
        top: Some(top.unwrap()),
        bottom: Some(bottom.unwrap()),
    };

    // Set S and T coordinate wrap mode, ClampToEdge will remove any possible seams on edges
    // of the skybox.
    for skybox_texture in skybox.textures().iter().filter_map(|t| t.clone()) {
        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

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

.with_skybox(create_skybox(resource_manager).await)

Also modify player creation in Game::new to this

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:

Controller

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.

Discussion: Reddit, Discord.