Skip to content

Рецепты скриптов ​

Каждый рецепт - готовый скрипт. Код на этой странице подтягивается прямо из тестов (crates/kvas_engine/tests/cookbook.rs): он компилируется и запускается в CI, поэтому не устаревает.

Чтобы использовать рецепт: скопируйте структуру в файл <проект>/scripts/src/, добавьте тип в register_scripts!(...) и перетащите скрипт на объект. Числовые поля с 0 по умолчанию задаются в Инспекторе (у Chaser это speed и stop_distance).

Вращение с настройкой в Инспекторе ​

Поле, объявленное в fields(), видно в Инспекторе и сохраняется в сцене.

rust
/// Spins the object around Y. `speed` is shown in the Inspector and saved in the scene.
#[derive(Default)]
pub struct Spinner {
    speed: f32,
}

impl Script for Spinner {
    fn update(&mut self, ctx: &mut Ctx) {
        ctx.rotate(Vec3::new(0.0, self.speed * ctx.dt(), 0.0));
    }

    fn fields(&mut self) -> Vec<Field<'_>> {
        vec![Field::f32("speed", &mut self.speed)]
    }
}

Преследователь ​

Идёт к объекту Player, поворачивается к нему и останавливается на дистанции. look_at_flat меняет только курс, move_towards возвращает true, когда точка достигнута.

rust
/// Walks towards the object named "Player" and stops at `stop_distance`.
#[derive(Default)]
pub struct Chaser {
    speed: f32,
    stop_distance: f32,
}

impl Script for Chaser {
    fn update(&mut self, ctx: &mut Ctx) {
        let Some(player) = ctx.find("Player") else { return };
        let Some(target) = ctx.position_of(player) else { return };
        ctx.look_at_flat(target);
        if ctx.distance_to(player).is_some_and(|d| d > self.stop_distance) {
            ctx.move_towards(target, self.speed);
        }
    }

    fn fields(&mut self) -> Vec<Field<'_>> {
        vec![Field::f32("speed", &mut self.speed), Field::f32("stop_distance", &mut self.stop_distance)]
    }
}

Патруль по точкам ​

Хранит маршрут и индекс следующей точки. Состояние (next) сбрасывается при горячей перезагрузке - это нормально.

rust
/// Walks between the points of `route` in a loop.
#[derive(Default)]
pub struct Patrol {
    route: Vec<Vec3>,
    next: usize,
    speed: f32,
}

impl Script for Patrol {
    fn start(&mut self, ctx: &mut Ctx) {
        if self.route.is_empty() {
            // Default route: a square around the start point.
            let o = ctx.position();
            self.route = vec![o + Vec3::X * 4.0, o + Vec3::new(4.0, 0.0, 4.0), o + Vec3::Z * 4.0, o];
        }
    }

    fn update(&mut self, ctx: &mut Ctx) {
        let target = self.route[self.next % self.route.len()];
        ctx.look_at_flat(target);
        if ctx.move_towards(target, self.speed.max(1.0)) {
            self.next += 1;
        }
    }
}

Спавнер шаров ​

Timer::repeating отсчитывает интервал, Rng даёт повторяемую случайность, despawn_after убирает мусор.

rust
/// Spawns a small physical ball every `interval` seconds at a random spot around the object.
#[derive(Default)]
pub struct BallSpawner {
    rng: Rng,
    timer: Option<Timer>,
}

impl Script for BallSpawner {
    fn start(&mut self, _ctx: &mut Ctx) {
        self.rng = Rng::new(7);
        self.timer = Timer::repeating(0.5);
    }

    fn update(&mut self, ctx: &mut Ctx) {
        let Some(timer) = self.timer.as_mut() else { return };
        if timer.tick_secs(ctx.dt()).just_finished() {
            let at = ctx.position() + self.rng.on_circle(3.0) + Vec3::Y * 4.0;
            let ball = ctx.spawn_primitive("Ball", "sphere", at);
            ctx.set_color(ball, [self.rng.f32(), self.rng.f32(), self.rng.f32(), 1.0]);
            ctx.despawn_after(ball, 10.0);
        }
    }
}

Камера от третьего лица ​

Экспоненциальное сглаживание 1 - exp(-k * dt) даёт одинаковое ощущение на 30 и 144 FPS.

rust
/// Smoothly follows the object named "Player" from behind and above.
#[derive(Default)]
pub struct FollowCamera {
    offset: Vec3,
}

impl Script for FollowCamera {
    fn start(&mut self, _ctx: &mut Ctx) {
        if self.offset == Vec3::ZERO {
            self.offset = Vec3::new(0.0, 4.0, 7.0);
        }
    }

    fn update(&mut self, ctx: &mut Ctx) {
        let Some(player) = ctx.find("Player") else { return };
        let Some(target) = ctx.position_of(player) else { return };
        let wanted = target + self.offset;
        let here = ctx.position();
        // Exponential smoothing is frame-rate independent: the same feel at 30 and 144 FPS.
        let smooth = here.lerp(wanted, 1.0 - (-6.0 * ctx.dt()).exp());
        ctx.set_position(smooth);
        ctx.look_at(target);
    }
}

Монета ​

Проверяет, кто находится рядом, по тегу player (тег добавляется в Инспекторе или ctx.add_tag).

rust
/// A coin: the player collects it by touching it.
#[derive(Default)]
pub struct Coin;

impl Script for Coin {
    fn update(&mut self, ctx: &mut Ctx) {
        ctx.rotate(Vec3::new(0.0, 120.0 * ctx.dt(), 0.0));
        let hits = ctx.overlap_sphere(ctx.position(), 0.8);
        if hits.into_iter().any(|e| ctx.has_tag(e, "player")) {
            ctx.hud_message("+1 coin", 1.0);
            ctx.despawn_self();
        }
    }
}

Выстрел лучом ​

raycast возвращает RayHit. Силу можно толкнуть через компонент RigidBody попавшего объекта.

rust
/// Press the left mouse button to shoot a ray forward and push what it hits.
#[derive(Default)]
pub struct Shooter {
    force: f32,
}

impl Script for Shooter {
    fn update(&mut self, ctx: &mut Ctx) {
        if !ctx.mouse_pressed(MouseButton::Left) {
            return;
        }
        let origin = ctx.position();
        let direction = ctx.forward();
        if let Some(hit) = ctx.raycast(origin, direction, 50.0) {
            ctx.hud_message("hit!", 0.5);
            // Apply the impulse to the hit object through the world.
            if let Some(body) = ctx.get_mut_of::<RigidBody>(hit.entity) {
                body.impulse += direction * self.force.max(5.0);
            }
        }
    }
}

Сообщения: кнопка и дверь ​

Скрипты не знают друг друга: кнопка рассылает сигнал, дверь его слушает. Сигналы доставляются в следующем кадре.

rust
/// A door that opens when somebody broadcasts the "open_door" signal.
#[derive(Default)]
pub struct Door {
    open: bool,
}

impl Script for Door {
    fn update(&mut self, ctx: &mut Ctx) {
        if !ctx.signals("open_door").is_empty() {
            self.open = true;
        }
        let target_y = if self.open { 3.0 } else { 0.0 };
        let y = ctx.transform().position.y;
        ctx.transform_mut().position.y = approach(y, target_y, 2.0 * ctx.dt());
    }
}

/// A button that sends the signal after being touched.
#[derive(Default)]
pub struct DoorButton;

impl Script for DoorButton {
    fn update(&mut self, ctx: &mut Ctx) {
        if ctx.action_pressed("Interact") {
            let _ = ctx.emit("open_door", json!({ "from": "button" }));
        }
    }
}

Деформация вершин и общие переменные ​

make_editable превращает встроенный примитив в редактируемую сетку, editable(e) даёт доступ к вершинам: wave, twist, taper, bend, noise_displace, smooth, inflate, выбор вершин select_sphere. set_global / global_f32 / add_global - переменные, общие для всех скриптов.

rust
/// Wobbles the vertices of a sphere like jelly and counts frames in a shared variable.
#[derive(Default)]
pub struct Jelly {
    ready: bool,
}

impl Script for Jelly {
    fn update(&mut self, ctx: &mut Ctx) {
        let me = ctx.entity;
        if !self.ready {
            self.ready = ctx.make_editable(me);
        }
        let t = ctx.elapsed();
        if let Some(mesh) = ctx.editable(me) {
            // Start from the original shape every frame, then bend it: cheap and drift-free.
            let mut fresh = kvas_engine::mesh_edit::EditableMesh::from_mesh(&kvas_engine::mesh::MeshData::primitive("sphere").unwrap()).unwrap();
            fresh.wave(Vec3::Y, Vec3::X, 0.1, 0.6, t);
            fresh.taper(Vec3::Y, 1.0, 1.0 + 0.2 * (t * 3.0).sin());
            mesh.vertices = fresh.vertices;
            mesh.changed();
        }
        // Any script can read this: `ctx.global_f32("jelly_frames", 0.0)`.
        ctx.add_global("jelly_frames", 1.0);
    }
}

Как проверить свой скрипт без редактора ​

Скрипты - обычные типы, поэтому их можно запускать в тесте, как делает этот cookbook:

rust
let host = ScriptHost::from_defs(vec![ScriptDef::of::<Chaser>("Chaser")]).unwrap();
let mut runtime = Runtime::new(scene, Some(host));
for _ in 0..240 {
    runtime.tick(1.0 / 60.0);
}
assert!(runtime.scene.world.get::<Transform>(chaser).unwrap().position.x > 5.0);

Kvas Engine