Weapons

In the previous tutorial we've added basic character controller, but what is a first-person shooter without weapons? Let's add them. In the end of the tutorial you should get something like this:

recoil

Weapon Prefab

At first, we need a 3D model for our weapon - use this ZIP-archive - it contains an M4 rifle 3D model that is prepared for direct usage in the engine. Unzip this archive in data/models folder. Now we can start by making a prefab for our weapon. Create a new scene (File -> NewScene) and find the m4.FBX 3D model in the Asset Browser and instantiate it in the scene by dragging it using mouse. Make sure to set location position of the weapon to (0, 0, 0). You should get something like this:

weapon prefab

This prefab is almost ready, all we need to do is to create a script for it that will contain a code for shooting.

Code

As usual, we need a script that will "drive" our weapons, run the following command at the root folder of your game:

fyrox-template script --name=weapon

Add the weapon mod to the lib.rs module using pub mod weapon;. This script will spawn projectiles and play shooting animation when we'll shoot the weapon. Let's add a "reference" to our projectile prefab that will be used for shooing:

#![allow(unused)]
fn main() {
    projectile: InheritableVariable<Option<ModelResource>>,
}

This field has quite complex type: InheritableVariable is used for property inheritance, Option is used to allow the field to be unassigned, and finally ModelResource is a reference to some projectile prefab. We'll assign this field later in the tutorial.

Next thing we need to define is a point from which the weapon will shoot. We can't just use the position of the weapon, because it will look unnatural if a projectile appear at a handle of the weapon or at some other place other than the barrel of the weapon. We'll use a child scene node of the weapon to define such point. Let's add the following field to the Weapon struct.

#![allow(unused)]
fn main() {
    shot_point: InheritableVariable<Handle<Node>>,
}

We'll assign this field later in the tutorial as well as projectile prefab.

Now we need some mechanism to "tell" the weapon to shoot, we could directly access the weapon script and call some shoot method, but in more or less complex game would almost certainly lead to lots of complaints from borrow checker. Instead of this, we'll use message passing mechanism - this will allow us to send a request for the weapon to shoot and the weapon will shoot when it will receive the message. Let's add a message for shooting in weapon.rs:

#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct ShootWeaponMessage {}
}

To actually be able to receive this message, we need to explicitly "subscribe" out script to it. Add the following code to the on_start method:

#![allow(unused)]
fn main() {
    fn on_start(&mut self, context: &mut ScriptContext) {
        context
            .message_dispatcher
            .subscribe_to::<ShootWeaponMessage>(context.handle);
    }
}

Every script has on_message method that is used for a message processing, we'll use it for shooting. Add the following code in the impl ScriptTrait for Weapon:

#![allow(unused)]
fn main() {
    fn on_message(
        &mut self,
        message: &mut dyn ScriptMessagePayload,
        ctx: &mut ScriptMessageContext,
    ) {
        // Check if we've received an appropriate message. This is needed because message channel is
        // common across all scripts.
        if message.downcast_ref::<ShootWeaponMessage>().is_some() {
            if let Some(projectile_prefab) = self.projectile.as_ref() {
                // Try to get the position of the shooting point.
                if let Some(shot_point) = ctx
                    .scene
                    .graph
                    .try_get(*self.shot_point)
                    .map(|point| point.global_position())
                {
                    // Shooting direction is just a direction of the weapon (its look vector)
                    let direction = ctx.scene.graph[ctx.handle].look_vector();

                    // Finally instantiate our projectile at the position and direction.
                    projectile_prefab.instantiate_at(
                        ctx.scene,
                        shot_point,
                        math::vector_to_quat(direction),
                    );
                }
            }
        }
    }
}

This code is pretty straightforward: at first, we're checking the message type, then we're checking if we have a prefab for projectiles. If so, we're fetching a position of the shot point scene node and finally instantiating the projectile prefab.

All is left to do is to register this script and assign it in the editor. To register the script, add the following code to the register method in lib.rs:

#![allow(unused)]
fn main() {
        context
            .serialization_context
            .script_constructors
            .add::<Weapon>("Weapon");
}

Start the editor and open m4.rgs prefab that we made at the beginning. Select the root node of the scene add Weapon script to it. Assign a Weapon:ShotPoint node to the Shot Point property:

weapon prefab final

The next thing we need to do is to create a prefab for projectile, that will be used for shooting.

Projectile

You mask ask - why we need a prefab for projectiles, why not just make a ray-based shooting? The answer is very simple - flexibility. Once we'll finish with this "complex" system, we'll get very flexible weapon system that will allow you to create weapons of any kind - it could be simple bullets, grenades, rockets, plasma, etc.

As usual, we need a prefab for our projectile. Create a new scene and add a Cylinder mesh scene node there, make sure to orient it along the Z axis (blue one) and adjust its XY scale to make it thin enough - this will be our "projectile". It will represent a bullet trail, but in reality the "bullet" will be represented by a simple ray cast and the trail will be extended to a point of impact. Overall your prefab should look like this:

bullet prefab

Select the root node of the prefab and set its lifetime to Some(0.1) - this will force the engine to remove the projectile automatically after 100 ms.

The projectile also needs its own script which will do a ray casting and other actions later in the tutorial, such as hit testing with enemies, etc. Create a new script by a well known command:

fyrox-template script --name=projectile

Add the projectile mod to the lib.rs module using pub mod projectile; and register it in register method:

#![allow(unused)]
fn main() {
        context
            .serialization_context
            .script_constructors
            .add::<Projectile>("Projectile");
}

Go to projectile.rs and add the following field to the Projectile struct:

#![allow(unused)]
fn main() {
    trail: InheritableVariable<Handle<Node>>,
}

This field will hold a handle to the trail (the red cylinder on the screenshot about) and we'll use this handle to borrow the node and modify the trail's length after ray casting.

The ray casting itself is the core of our projectiles, add the following code to the on_start method:

#![allow(unused)]
fn main() {
    fn on_start(&mut self, ctx: &mut ScriptContext) {
        let this_node = &ctx.scene.graph[ctx.handle];
        let this_node_position = this_node.global_position();

        // Cast a ray in from the node in its "look" direction.
        let mut intersections = Vec::new();
        ctx.scene.graph.physics.cast_ray(
            RayCastOptions {
                ray_origin: this_node_position.into(),
                ray_direction: this_node.look_vector(),
                max_len: 1000.0,
                groups: Default::default(),
                // Sort results of the ray casting so the closest intersection will be in the
                // beginning of the list.
                sort_results: true,
            },
            &mut intersections,
        );

        let trail_length = if let Some(intersection) = intersections.first() {
            // If we got an intersection, scale the trail by the distance between the position of the node
            // with this script and the intersection position.
            this_node_position.metric_distance(&intersection.position.coords)
        } else {
            // Otherwise the trail will be as large as possible.
            1000.0
        };

        if let Some(trail_node) = ctx.scene.graph.try_get_mut(*self.trail) {
            let transform = trail_node.local_transform_mut();
            let current_trail_scale = **transform.scale();
            transform.set_scale(Vector3::new(
                // Keep x scaling.
                current_trail_scale.x,
                trail_length,
                // Keep z scaling.
                current_trail_scale.z,
            ));
        }
    }
}

This code is pretty straightforward - at first we're borrowing the node of the projectile, saving its global position in a variable and then casting a ray from the position and in the "look" direction of the projectile. Finally, we're taking the first intersection from the list (it will be the closest one) and adjusting the trail's length accordingly.

The final step is to assign the script and its variables in the editor. Run the editor, open bullet.rgs (or how your prefab is called) prefab and select the root node, set Projectile script to it and set trail field to the Trail node. It should look like so:

bullet properties

Gluing Everything Together

We have everything ready for final tuning - in this section of the tutorial we'll finish putting everything together and will have a fully functioning weapon. Let's start from our weapon prefab, we need to "inform" it about the projectile prefab we've just made. Open the m4.rgs prefab of our weapon and find projectile field in the Weapon script there. Now find the bullet.rgs prefab of the projectile and drag'n'drop it onto the projectile field to set the value of it:

weapon prefab projectile

The last step is to add the weapon to the player. Open the player.rgs prefab and find the m4.rgs prefab in the Asset Browser, instantiate it in the scene and make it a child of the camera node. Overall it should look like this:

weapon in player

We almost finished our final preparations, you can even open scene.rgs and hit Play and see the weapon in game:

weapon in game

However, it won't shoot just yet - we need to send a message to the weapon for it to shoot. To do that, at first, we need to know to which weapon we'll send a request to shoot. It is very easy to do by using weapon's node handle. Add the following field to the Player struct:

#![allow(unused)]
fn main() {
    #[visit(optional)]
    current_weapon: InheritableVariable<Handle<Node>>,
}

We'll send a request to shoot in reaction to left mouse button clicks. To do that, go to player.rs and add the following code to the on_os_event:

#![allow(unused)]
fn main() {
        if let Event::WindowEvent {
            event:
                WindowEvent::MouseInput {
                    state,
                    button: MouseButton::Left,
                    ..
                },
            ..
        } = event
        {
            self.shoot = *state == ElementState::Pressed;
        }
}

And the following code to the on_update:

#![allow(unused)]
fn main() {
        if self.shoot {
            ctx.message_sender
                .send_to_target(*self.current_weapon, ShootWeaponMessage {});
        }
}

The last step is to assign the handle to the current weapon in the player's prefab. Open the player.rgs prefab in the editor and in the Player script find the Current Weapon field and assign to the Weapon node like so:

current weapon assignment

Run the game, and you should be able to shoot from the weapon, but it shoots way too fast. Let's make the weapon to shoot with desired interval while we're holding the mouse button. Add the two timer variables to the Weapon struct:

#![allow(unused)]
fn main() {
    shot_interval: InheritableVariable<f32>,

    #[reflect(hidden)]
    shot_timer: f32,
}

The shot_timer variable will be used to measure time between shots and the shot_interval will set the desired period of shooting (in seconds). We'll handle one of these variables in on_update method:

#![allow(unused)]
fn main() {
    fn on_update(&mut self, context: &mut ScriptContext) {
        self.shot_timer -= context.dt;
    }
}

This code is very simple - it just decreases the timer and that's all. Now let's add a new condition to the on_message method right after if message.downcast_ref::<ShootWeaponMessage>().is_some() { line:

#![allow(unused)]
fn main() {
            if self.shot_timer >= 0.0 {
                return;
            }
            // Reset the timer, this way the next shot cannot be done earlier than the interval.
            self.shot_timer = *self.shot_interval;
}

Open the m4.rgs prefab in the editor and set the interval in the Weapon script to 0.1. Run the game and the weapon should shoot less fast.

Bells and Whistles

We can improve overall feeling of our weapon by adding various effects.

Trail Dissolving

Our shot trails disappear instantly and this looks unnatural. It can be fixed very easy by using animations. Read the docs about the animation editor first to get familiar with it. Open the bullet.rgs prefab, add Animation Player node to the prefab and open the animation editor. Add a new track that binds to the alpha channel of the color of the trail's material:

trail animation

Also, make sure the Unique Material check box is checked in the material property of the trail's mesh. Otherwise, all trails will share the same material and once the animation is finished, you won't see the trail anymore. Run the game and shot trails should disappear smoothly.

Impact Effects

Right now our projectiles does not interact with world, we can improve that by creating sparks effect at the point of impact. Download this pre-made effect and unzip it in data/effects folder.

Add the following field to the Projectile struct:

#![allow(unused)]
fn main() {
    impact_effect: InheritableVariable<Option<ModelResource>>,
}

This is a "link" to particle effect, that we'll spawn at the impact position. Let's add this code to the end of on_start of impl ScriptTrait for Projectile:

#![allow(unused)]
fn main() {
        if let Some(intersection) = intersections.first() {
            if let Some(effect) = self.impact_effect.as_ref() {
                effect.instantiate_at(
                    ctx.scene,
                    intersection.position.coords,
                    math::vector_to_quat(intersection.normal),
                );
            }
        }
}

The last thing we need to do is to assign Impact Effect property in bullet.rgs to the pre-made effect. Run the game, and you should see something like this when shooting:

shooting

World Interaction

In this section we'll add an ability to push physical objects by shooting. All we need to do is to add the following code to at the end of on_start of impl ScriptTrait for Projectile:

#![allow(unused)]
fn main() {
        if let Some(intersection) = intersections.first() {
            if let Some(collider) = ctx.scene.graph.try_get(intersection.collider) {
                let rigid_body_handle = collider.parent();
                if let Some(rigid_body) = ctx
                    .scene
                    .graph
                    .try_get_mut_of_type::<RigidBody>(rigid_body_handle)
                {
                    if let Some(force_dir) = (intersection.position.coords - this_node_position)
                        .try_normalize(f32::EPSILON)
                    {
                        let force = force_dir.scale(200.0);

                        rigid_body.apply_force_at_point(force, intersection.position.coords);
                        rigid_body.wake_up();
                    }
                }
            }
        }
}

This code is very straightforward: at first, we're taking the closest intersection and by using its info about collider taking a reference to the rigid body we've just hit by the ray. Next, we're applying force at the point of impact, which will push the rigid body.

To check how it works, unzip this prefab to data/models and add some instances of it to the scene.rgs and run the game. You should see something like this:

pushing

Recoil

The final improvement that we could do is to add a recoil to our weapon. We'll use animation for that, like we did for trails. Instead of animation the color, we'll animation position of the weapon model. Open m4.rgs prefab, add an animation player, create a new animation, add a binding to Position property of m4.FBX node with the following parameters:

recoil animation

Now we need a way to enable this animation when shooting, to do that we need to know a handle of the animation player in the weapon script. Let's add it to the Weapon struct:

#![allow(unused)]
fn main() {
    animation_player: InheritableVariable<Handle<Node>>,
}

Add the following code to the on_message in weapon.rs, right after the shooting condition (if self.shot_timer >= 0.0 { ...):

#![allow(unused)]
fn main() {
            if let Some(animation_player) = ctx
                .scene
                .graph
                .try_get_mut_of_type::<AnimationPlayer>(*self.animation_player)
            {
                if let Some(animation) = animation_player
                    .animations_mut()
                    .get_value_mut_silent()
                    .iter_mut()
                    .next()
                {
                    animation.rewind();
                    animation.set_enabled(true);
                }
            }
}

Run the game, and you should see something like this when shooting:

recoil

Conclusion

In this tutorial part we've added weapons that can shoot projectiles, which in their turn can interact with the environment.