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

Cameras

Use case: a third person camera on a spring arm

The goal: a camera behind the character that orbits with the mouse and pulls in when a wall is in the way.

#![allow(unused)]
fn main() {
use bindings::engine::{CameraComponent, CameraComponentExt, SpringArmComponent, SpringArmComponentExt};

#[uclass(parent = Character)]
pub struct Hero {
    #[component(attach = "root_component")]
    camera_boom: SpringArmComponent,

    #[component(attach = "camera_boom", socket = "SpringEndpoint")]
    follow_camera: CameraComponent,
}

#[uclass_impl]
impl Hero {
    #[class_defaults]
    fn class_defaults(&mut self) -> RustealResult<()> {
        let me = self.as_ref().checked()?;
        me.set_use_controller_rotation_pitch(false);
        me.set_use_controller_rotation_yaw(false);
        me.set_use_controller_rotation_roll(false);

        let boom = self.camera_boom()?.checked()?;
        boom.set_target_arm_length(400.0);
        boom.set_use_pawn_control_rotation(true);

        self.follow_camera()?.checked()?.set_use_pawn_control_rotation(false);

        Ok(())
    }
}
}

The spring arm follows the controller’s rotation, which the look input changes; the character does not turn with it, and the movement component turns it toward where it walks (set_orient_rotation_to_movement(true)).

A smoother camera adds lag: boom.set_enable_camera_lag(true) and boom.set_camera_lag_speed(10.0).

Use case: a first person camera

The goal: a camera at the head, with arms only the player sees and a body only others see.

#![allow(unused)]
fn main() {
use bindings::engine::{
    CameraComponent, CameraComponentExt, EFirstPersonPrimitiveType, PrimitiveComponentExt,
    SceneComponentExt, SkeletalMeshComponent
};

#[uclass(parent = Character)]
pub struct FirstPersonHero {
    #[component(attach = "mesh", name = "First Person Mesh")]
    first_person_mesh: SkeletalMeshComponent,

    #[component(attach = "first_person_mesh", socket = "head", name = "First Person Camera")]
    first_person_camera: CameraComponent,
}

#[uclass_impl]
impl FirstPersonHero {
    #[class_defaults]
    fn class_defaults(&mut self) -> RustealResult<()> {
        let arms = self.first_person_mesh()?.checked()?;
        arms.set_only_owner_see(true);
        arms.set_first_person_primitive_type(EFirstPersonPrimitiveType::FirstPerson);
        arms.set_collision_profile_name(FName::new("NoCollision").handle(), Some(true));

        let camera = self.first_person_camera()?.checked()?;
        camera.set_use_pawn_control_rotation(true);
        camera.set_enable_first_person_field_of_view(true);
        camera.set_first_person_field_of_view(70.0);

        let body = self.as_ref().checked()?.get_mesh().checked()?;
        body.set_owner_no_see(true);
        body.set_first_person_primitive_type(EFirstPersonPrimitiveType::WorldSpaceRepresentation);

        Ok(())
    }
}
}

The Blueprint child assigns the arms’ and the body’s skeletal meshes and Animation Blueprints.

Use case: limiting how far the player looks up and down

The goal: a camera manager that clamps the pitch.

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

#[uclass(parent = PlayerCameraManager)]
pub struct MyCameraManager {}

#[uclass_impl]
impl MyCameraManager {
    #[class_defaults]
    fn class_defaults(&mut self) -> RustealResult<()> {
        let me = self.as_ref().checked()?;
        me.set_view_pitch_min(-70.0);
        me.set_view_pitch_max(80.0);
        Ok(())
    }
}
}

The player controller uses it, set in its own class defaults:

#![allow(unused)]
fn main() {
self.as_ref().checked()?.set_player_camera_manager_class(
    SubclassOf::<MyCameraManager>::base().upcast_to(),
);
}

Use case: a side scroller camera computed every frame

The goal: a camera that follows the character along X, stays within the level’s bounds, and moves up and down smoothly.

A camera manager decides the camera’s location, rotation and field of view every frame in BlueprintUpdateCamera:

#![allow(unused)]
fn main() {
#[uclass(parent = PlayerCameraManager)]
pub struct SideCameraManager {
    #[uproperty(EditAnywhere, default = 400.0)]
    current_zoom: f32,

    #[uproperty(EditAnywhere, default = -1000.0)]
    camera_x_min_bounds: f32,

    #[uproperty(EditAnywhere, default = 1000.0)]
    camera_x_max_bounds: f32,
}

#[uclass_impl]
impl SideCameraManager {
    #[ufunction(Override)]
    fn blueprint_update_camera(
        &mut self,
        camera_target: UObjectRef<Actor>,
        new_camera_location: UStructRef<FVector>,
        new_camera_rotation: UStructRef<FRotator>,
        new_camera_fov: OutRef<f32>,
    ) -> bool {
        let Ok(target) = camera_target.checked() else {
            return false;
        };

        let location = target.k2_get_actor_location().to_dvec3();
        let x = location.x.clamp(f64::from(self.camera_x_min_bounds()), f64::from(self.camera_x_max_bounds()));

        new_camera_location.set_x(x);
        new_camera_location.set_y(location.y + f64::from(self.current_zoom()));
        new_camera_location.set_z(location.z);

        new_camera_rotation.set_pitch(0.0);
        new_camera_rotation.set_yaw(-90.0);
        new_camera_rotation.set_roll(0.0);

        new_camera_fov.set(65.0);

        true
    }
}
}

The event writes the camera into the structs it receives (UStructRef, written in place) and the field of view into an OutRef, and returns true to use them. The side-scrolling template adds smoothing with the engine’s interpolation formulas, written as plain Rust in its model.rs and tested there.