Рецепты скриптов
Каждый рецепт - готовый скрипт. Код на этой странице подтягивается прямо из тестов (crates/kvas_engine/tests/cookbook.rs): он компилируется и запускается в CI, поэтому не устаревает.
Чтобы использовать рецепт: скопируйте структуру в файл <проект>/scripts/src/, добавьте тип в register_scripts!(...) и перетащите скрипт на объект. Числовые поля с 0 по умолчанию задаются в Инспекторе (у Chaser это speed и stop_distance).
Вращение с настройкой в Инспекторе
Поле, объявленное в fields(), видно в Инспекторе и сохраняется в сцене.
/// 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, когда точка достигнута.
/// 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) сбрасывается при горячей перезагрузке - это нормально.
/// 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 убирает мусор.
/// 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.
/// 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).
/// 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 попавшего объекта.
/// 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);
}
}
}
}Сообщения: кнопка и дверь
Скрипты не знают друг друга: кнопка рассылает сигнал, дверь его слушает. Сигналы доставляются в следующем кадре.
/// 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 - переменные, общие для всех скриптов.
/// 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:
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);