Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Projectiles and weapons

Use case: a projectile

The goal: a bullet that flies, bounces, damages what it hits, and is removed after a while.

#![allow(unused)]
fn main() {
use bindings::engine::{
    Character, DamageType, ECollisionEnabled, ECollisionResponse, FHitResult, FHitResultExt,
    GameplayStatics, PawnExt, PrimitiveComponent, PrimitiveComponentExt,
    ProjectileMovementComponent, ProjectileMovementComponentExt, SphereComponent,
    SphereComponentExt
};

#[uclass(parent = Actor)]
pub struct Bullet {
    #[component(root, name = "Collision Component")]
    collision: SphereComponent,

    #[component(name = "Projectile Movement")]
    movement: ProjectileMovementComponent,

    #[uproperty(EditAnywhere, category = "Hit", default = 25.0)]
    hit_damage: f32,

    #[uproperty(EditAnywhere, category = "Hit", default = 100.0)]
    physics_force: f32,

    hit: bool,
}

#[uclass_impl]
impl Bullet {
    #[class_defaults]
    fn class_defaults(&mut self) -> RustealResult<()> {
        let collision = self.collision()?.checked()?;
        collision.set_sphere_radius(16.0, Some(true));
        collision.set_collision_enabled(ECollisionEnabled::QueryAndPhysics);
        collision.set_collision_response_to_all_channels(ECollisionResponse::ECR_Block);

        let movement = self.movement()?.checked()?;
        movement.set_initial_speed(3000.0);
        movement.set_max_speed(3000.0);
        movement.set_should_bounce(true);

        self.as_ref().checked()?.set_initial_life_span(5.0);

        Ok(())
    }

    #[ufunction(Override)]
    fn receive_begin_play(&mut self) {
        if let (Ok(collision), Ok(me)) = (self.collision().and_then(|c| c.checked()), self.as_ref().checked()) {
            collision.ignore_actor_when_moving(me.get_instigator().upcast_to(), true);
        }
    }

    #[ufunction(Override)]
    #[allow(clippy::too_many_arguments)]
    fn receive_hit(
        &mut self,
        _my_comp: UObjectRef<PrimitiveComponent>,
        other: UObjectRef<Actor>,
        other_comp: UObjectRef<PrimitiveComponent>,
        _self_moved: bool,
        _hit_location: UStructRef<FVector>,
        _hit_normal: UStructRef<FVector>,
        _normal_impulse: UStructRef<FVector>,
        hit: UStructRef<FHitResult>,
    ) {
        if self.hit() {
            return;
        }

        self.set_hit(true);

        let Ok(me) = self.as_ref().checked() else {
            return;
        };

        if other.cast::<Character>().is_ok() {
            let instigator = me.get_instigator().checked().map(|pawn| pawn.get_controller()).unwrap_or_default();
            GameplayStatics::apply_damage(other, self.hit_damage(), instigator, me.as_ref().upcast_to(), SubclassOf::<DamageType>::base());
        }

        if let Ok(component) = other_comp.checked()
            && component.is_simulating_physics(None)
        {
            let direction = -hit.get_impact_normal().to_dvec3();
            component.add_impulse_at_location(
                &FVector::from_dvec3(direction * f64::from(self.physics_force())),
                &FVector::from_dvec3(hit.get_impact_point().to_dvec3()),
                None,
            );
        }

        self.bp_on_hit(&hit.to_owned());
        me.k2_destroy_actor();
    }

    #[ufunction(BlueprintImplementableEvent, name = "BP_OnProjectileHit")]
    fn bp_on_hit(&self, hit: &OwnedStruct<FHitResult>) {}
}
}
  • The ProjectileMovementComponent moves it; the root sphere collides.
  • set_initial_life_span removes it after five seconds if it hits nothing.
  • The Blueprint child gives it a mesh, and implements BP On Projectile Hit for the impact effect and sound.

The weapon spawns it deferred, so the instigator is set before BeginPlay runs (see Spawning actors).

Use case: a weapon with a magazine and a refire rate

The goal: a weapon that fires while the trigger is held (or once per press), counts its bullets, and tells the HUD.

The shooter template splits it in two: the rules in model.rs, tested without the engine, and the weapon class that applies them.

#![allow(unused)]
fn main() {
pub enum TriggerPull {
    FireNow,
    FireIn(f32),
    Wait,
}

pub fn pull_trigger(time_since_last_shot: f32, refire_rate: f32, full_auto: bool) -> TriggerPull {
    if time_since_last_shot > refire_rate {
        TriggerPull::FireNow
    } else if full_auto {
        TriggerPull::FireIn(refire_rate - time_since_last_shot)
    } else {
        TriggerPull::Wait
    }
}

pub fn bullets_after_shot(bullets: i32, magazine_size: i32) -> i32 {
    let left = bullets - 1;
    if left <= 0 { magazine_size } else { left }
}
}
#![allow(unused)]
fn main() {
pub fn start_firing(&mut self) {
    self.set_is_firing(true);

    let now = GameplayStatics::get_time_seconds(self.as_ref().upcast_to()) as f32;

    match model::pull_trigger(now - self.time_of_last_shot(), self.refire_rate(), self.b_full_auto()) {
        TriggerPull::FireNow => self.fire(),
        TriggerPull::FireIn(delay) => self.set_refire_timer("Fire", delay),
        TriggerPull::Wait => {}
    }
}

pub fn stop_firing(&mut self) {
    self.set_is_firing(false);
    KismetSystemLibrary::k2_clear_timer(self.as_ref().upcast_to(), "Fire");
}
}

fire spawns the projectile, plays the firing montage on the holder, adds recoil (add_controller_pitch_input), updates the bullet count and sets the timer for the next shot. See shooter/weapon.rs in a first-person --variant shooter project.

Use case: attaching a weapon to a hand

The goal: the weapon actor follows the character’s hand.

#![allow(unused)]
fn main() {
use bindings::engine::{EAttachmentRule, SceneComponentExt};

let snap = EAttachmentRule::SnapToTarget;

weapon.checked()?.k2_attach_to_actor(me.as_ref().upcast_to(), FName::NONE.handle(), snap, snap, snap, false);

weapon_mesh.checked()?.k2_attach_to_component(
    character_mesh.upcast_to(),
    FName::new("HandGrip_R").handle(),
    snap,
    snap,
    snap,
    false,
);
}

The weapon is spawned with the character as its owner, and destroys itself with it:

#![allow(unused)]
fn main() {
let weapon = self.as_ref();

me.get_owner()
    .checked()?
    .on_destroyed()
    .add(move |_destroyed| {
        if let Ok(weapon) = weapon.checked() {
            weapon.k2_destroy_actor();
        }
    })?
    .detach();
}

Use case: telling the AI a shot was heard

#![allow(unused)]
fn main() {
me.make_noise(
    Some(1.0),
    Some(pawn_owner),
    &pawn.k2_get_actor_location(),
    Some(3000.0),
    Some(FName::new("Shot").handle()),
);
}

make_noise is heard by AI perception’s hearing sense, with the tag the AI filters on. See AI.