#godot #animation #tweening #spire #api-bindings #gdextension

nightly spire_tween

一个针对 Godot(4.2+) 的粘性缓动库,灵感来自 DoTween(Unity 的第三方包)

4 个版本

新版本 0.2.1 2024年8月18日
0.2.0 2024年8月9日
0.1.2 2024年7月5日
0.1.1 2024年7月2日
0.1.0 2024年7月1日

#447 in 游戏开发

Download history 400/week @ 2024-06-29 31/week @ 2024-07-06 2/week @ 2024-07-13 9/week @ 2024-07-27 74/week @ 2024-08-03 36/week @ 2024-08-10

119 个月下载

MIT 许可证

135KB
4K SLoC

SpireTween - Godot 4.2+ 的替代缓动库

警告!

  • 此包未经彻底测试,可能包含破坏游戏的功能。尽管如此,我每天都在所有项目中使用它,我承诺会尽快修复任何报告的错误,至少到2026年。
  • 此包基本没有文档,这个readme就是所有内容,并且它可能会保持这样,因为作者是唯一的已知用户。接受添加文档的PR。
  • 此包需要Rust nightly,并使用以下不稳定功能
#![feature(inline_const_pat)]
#![feature(trait_alias)]
#![feature(hash_extract_if)]
#![feature(let_chains)]
#![feature(is_none_or)]

摘要

为什么

Spire Tween 与 Godot Tween 的比较

主要原因:人体工程学

比较 1:基本缓动

目标

  1. 使用缓动使 CanvasItem 的颜色
  2. 打印消息

Godot 缓动

fn basic_godot(mut node: Gd<Node2D>) {
	let mut godot_tween =
		node.create_tween()
		    .unwrap();	// unwrap 1

	let property_tweener =
		godot_tween
			.tween_property(
				node.clone().upcast(),	// boilerplate
				"modulate".into(),	// Requires manually typing property name
				Color::from_rgb(1.0, 0.0, 0.0).to_variant(),	// No type safety
				2.0
			)
			.unwrap()	// unwrap 2
			.set_ease(EaseType::OUT)
			.unwrap();	// unwrap 3

	let err =	// Will error if you make any typos
		godot_tween.connect(
			"finished".into(),	// boilerplate
			Callable::from_fn(
				"print",	// boilerplate
				|_| {
					godot_print!("Tween finished");
					Ok(Variant::nil())	// boilerplate
				},
			));
}

Spire 缓动

fn basic_spire(node: &Gd<Node2D>) {
	let spire_handle =		// No errors, internally calls `bind_mut` in Singleton, will panic if you violate Rust's mutability rules, more info at the end of this README
		node.do_color(Color::from_rgb(1.0, 0.0, 0.0), 2.0)	// Type safe, no variants 
		    .with_ease(Ease::Out)
		    .on_finish(|| {	// Rust compiler doesn't allow making typos
			    godot_print!("Tween finished");
		    }).register();
}

比较 2:执行一系列缓动

目标:(使 CanvasItem "闪烁")

  1. 使用缓动使 CanvasItem 的颜色从黑色 => 白色
  2. 使用缓动使 CanvasItem 的颜色从白色 => 黑色

Godot 缓动

fn sequence_godot(mut node: Gd<Node2D>) {
	let mut godot_tween = 
		node.create_tween()
		    .unwrap();	// unwrap 1
	
	// 1st tween
	godot_tween
		.tween_property(
			node.clone().upcast(),	// boilerplate
			"modulate".into(),	// No type safety, typos can happen
			Color::WHITE.to_variant(),	// No type safety
			2.0
		)
		.unwrap()	// unwrap 2 
		.set_ease(EaseType::IN_OUT)
		.unwrap()	// unwrap 3
		.from(Color::BLACK.to_variant());	// No type safety
	
	// 2nd tween
	godot_tween
		.tween_property(
			node.clone().upcast(),	// boilerplate
			"modulate".into(),	// No type safety, typos can happen
			Color::BLACK.to_variant(),	// No type safety
			2.0,
		)
		.unwrap()	// No type safety
		.set_ease(EaseType::OUT_IN);
}

Spire 缓动

fn sequence_spire(node: &Gd<Node2D>) {
	let mut sequence = SpireSequence::new();
	sequence.append(
		node.do_color(Color::WHITE, 2.0)	// Type safe
		    .starting_at(Color::BLACK)	// Type safe
	);

	sequence.append(node.do_color(Color::BLACK, 2.0));	// Type safe
	
	let handle = sequence.register();	// No unwrap
}

比较 3:并行执行缓动

目标

  1. 同时使用缓动调整 CanvasItem 的缩放和颜色
  2. 使用缓动将 CanvasItem 的缩放恢复到正常

Godot 缓动

fn parallel_godot(mut node: Gd<Node2D>) {
	let mut godot_tween = 
		node.create_tween()
		    .unwrap();	// unwrap 1

	godot_tween.set_parallel();
	
	godot_tween
		.tween_property(
			node.clone().upcast(),	// boilerplate
			"modulate".into(),	// No type safety, typos can happen
			Color::RED.to_variant(),	// No type safety
			2.0,
		)
		.unwrap()	// unwrap 2
		.set_ease(EaseType::OUT);
	
	godot_tween
		.tween_property(
			node.clone().upcast(),	// boilerplate
			"scale".into(),	// No type safety, typos can happen
			Vector2::new(5.0, 5.0).to_variant(),	// No type safety
			2.0,
		)
		.unwrap()	// unwrap 3
		.set_ease(EaseType::OUT);
	
	godot_tween.chain();
	
	godot_tween
		.tween_property(
			node.clone().upcast(),	// boilerplate
			"scale".into(),	// No type safety, typos can happen
			Vector2::new(1.0, 1.0).to_variant(),	// No type safety
			2.0,
		)
		.unwrap()	// unwrap 4
		.set_ease(EaseType::IN);
}

Spire 缓动

fn parallel_spire(node: &Gd<Node2D>) {
	let mut sequence = SpireSequence::new();
	sequence.append(
		node.do_color(Color::RED, 2.0)	// Type safe, typos don't compile
			.with_ease(Ease::Out)
	);
	
	sequence.join(
		node.do_scale(Vector2::new(5.0, 5.0), 2.0)	// Type safe, typos don't compile
			.with_ease(Ease::Out)
	);
	
	sequence.append(
		node.do_scale(Vector2::new(1.0, 1.0), 2.0)	// Type safe, typos don't compile
			.with_ease(Ease::In)
	);
	
	let handle = sequence.register();
}

比较 4:配置缓动

Godot 缓动

fn using_godot_tween(mut node: Gd<Node2D>) {
	let mut godot_tween =
		node.create_tween()
		    .unwrap()	// unwrap 1
		    .set_loops_ex()	// Many settings require boilerplate builder patterns
		    .loops(5)
		    .done()
		    .unwrap()	// unwrap 2 
		    .set_process_mode(TweenProcessMode::IDLE)
		    .unwrap()	// unwrap 3
		    .set_speed_scale(2.0)
		    .unwrap()	// unwrap 4
		    .set_pause_mode(TweenPauseMode::BOUND)
		    .unwrap();	// unwrap 5
	
	// Tweening a property
	let mut property_tweener =
		godot_tween
			.tween_property(
				node.clone().upcast(),	// boilerplate
				"modulate".into(),	// Requires manually typing property name
				Color::from_rgb(1.0, 0.0, 0.0).to_variant(),	// No type safety
				2.0,
			).unwrap();	// unwrap 6
	
	property_tweener
		.as_relative()
		.unwrap()	// unwrap 7
		.set_ease(EaseType::IN)// limited to Godot's Ease modes
		.unwrap()	// unwrap 8
		.set_delay(5.0)
		.unwrap();	// unwrap 9
	
	godot_tween
		.connect_ex(
			"finished".into(),	// boilerplate
			Callable::from_object_method(&node, "on_finished"),
		)
		.flags(object::ConnectFlags::DEFERRED.ord() as u32)
		.done();
	
	// Creating a sequence
	godot_tween
		.tween_method(
			Callable::from_object_method(&node, "tweenable_method"),
			2.to_variant(),	// No type safety
			10.to_variant(),	// No type safety
			5.0
		).unwrap();	// unwrap 10
}

Spire 缓动

fn configuring_spire(node: &Gd<Node2D>) {
	let spire_tween =
		node.do_color(Color::from_rgb(1.0, 0.0, 0.0), 2.0)	// Type safe, no variants
			// Just like in Godot, you don't have to manually set these if you want to use defaults
			// this is just to show that you can
			.looped(5)
		    .with_process_mode(TweenProcessMode::IDLE)
		    .with_pause_mode(TweenPauseMode::BOUND)
		    .with_delay(5.0)
		    .with_speed_scale(2.0)
		    .with_ease(Ease::In)	// use `keyframe` crate for easing, providing commonly-used easing as well as allowing custom ones
		    .as_relative(Color::BLACK)	// relative allows setting a custom origin
		    .as_speed_based(20.0)
			// do_color automatically binds its lifetime to `node`, the object you called this on, but you can also 
			// bind it to other godot objects instead of just `node`
			.bound_to(other_object)
		    .on_finish(|| { godot_print!("Finished"); })	// direct closures
		    .on_finish(Callable::from_object_method(&node, "on_finish"));	// callables work too

	let mut handle
		: SpireHandle<Property<Color>>	// Handle knows exact tween type, providing type safety 
		= spire_tween.register();	// Same as `done()`, only needs to be called once, no unwrapping

	// You can use the handle to read/mutate the tween on the fly
	// Handles don't guarantee that what they point at exists
	let fetch_result: Result<(), FetchError> =
		handle.on_finish(|| {
			godot_print!("Managed to connect on_finish on the fly!")
		});

	match fetch_result {
		Ok(()) => {}
		Err(fetch_err) => {
			match fetch_err {	// Error explaining exactly what went wrong
				FetchError::NotFound => {}
				FetchError::TypeMismatch { expected, found } => {
					godot_error!("Could not fetch tween expected type {expected:?}, found {found:?}");
				}
			}
		}
	}

	// Handles allow you to perform almost all the actions you can perform as if you're building the tween
	// Everything returns a result though
	let _ = handle.stop();

	// If you want to access the tween directly, you can call map
	let fetch_result: Result<TweenState, FetchError> =
		handle.map(|tween: &mut SpireTween<Property<Color>>| {
			tween.t.ease = Ease::In;
			tween.delay = 2.0;
			// ..

			// Need to read something on the tween? Return it on map:
			tween.state()
		});
}

它是如何工作的

register() 尝试访问 TweensController 自加载,将其缓动插入其内部列表。它还返回一个 SpireHandle<T>,您可以使用它稍后访问缓动,允许您读取其状态,实时更改设置等。

如果无法访问 cTweensController,操作将 panic,这可能会发生如果它不存在,或者如果它在其他地方已被借用。

但是,只要遵循README文件末尾提到的某些规则,这种情况不应该发生。

SpireTween提供了几种方法来配置你的缓动行为。尽管该库没有文档说明,但名称相当直观,遵循模式:“do_”+ godot_property_name。

在注册缓动之前,你应该完成所有配置。在注册后修改缓动可能导致不可预测的行为。我不建议尝试在Godot主线程之外注册/访问缓动。

更多示例

let node: Gd<Node2D> = ..;
let mut tween = owner.do_move_x(2000.0, 5.0);

let origin = 0.0; // since relative tweens don't have a starting point, the origin parameter is necessary for the tween to compare how much it needs to move between each frame
tween = tween.as_relative(origin);

// If you want to set a specific starting point: (if you don't, the starting point will be evaluated when the tween starts.
tween = tween.starting_at(500.0);

tween = tween.with_delay(10.0);

let speed = 100.0; // Speed is always in units per second, 100. speed means position:x increases by 100 each second.
tween = tween.as_speed_based(speed);

你可以将这些方法串联起来

let handle =
	node.do_global_position(Vector2::new(1920., 1080.), 5.0)
		.with_delay(10.0)
		.starting_at(Vector2::new(0., 0.))
		.looped(10)
		.on_finish(|| { godot_print!("Tween Finished"); })
		.register();

属性缓动器仅仅是[DoProperty](https://github.com/Houtamelo/spire_tween/blob/main/src/extensions/property/mod.rs)特征的包装器。 do_property自动将节点绑定到缓动上,这意味着当节点被删除时,缓动将自动“死亡”。

DoProperty使用set_indexed(),因此属性路径也是有效的

// Tween only the Z position
let tween = node.do_property("position:z", 20., 10.);

如果您想使用库原生不支持的价值,您可以使用DoVarProperty/DoVarMethod

let my_custom_property = ..;
let my_custom_end_val = ..;
let duration = 5.0;

// the only difference is that the method is called do_property_var()
let tween = node.do_var_property(my_custom_property, my_custom_end_val, duration);

您还可以缓动方法

// Imagine you have a method like this...
#[func]
fn _set_fill(&mut self, value: f64) {
	self.base_mut().set_value(value);
}

// You can tween it like this
let start = 0.;
let end = 1.;
let duration = 8.;
let tween = node.do_method("_set_fill", start, end, duration);

如果您只需延迟调用一次方法,您可以

#[func]
fn _start_game(&mut self, player_name: String, difficulty: Difficulty) {
	...
}

let player_name = "Houtamelo";
let difficulty = Difficulty::GameDeveloper;
let delay = 24.;
let args = Array::from(&[player_name.to_variant(), difficulty.to_variant()]);

let callable = Callable::from_object_method(node, "_start_game").bindv(args);

let tween = node.do_delayed_call(callable, delay);

// Closures are also accepted:
let tween = node.do_delayed_call(|| node._start_game(player_name, difficulty), delay);

永远不要忘记注册您的缓动!否则它们将被丢弃

let handle = tween.register();

我们还有序列,它们的工作方式非常类似于DoTween的

let mut sequence = SpireSequence::new();
sequence.append(owner.do_move_x(640.0, 5.0));
sequence.join(owner.do_fade(1.0, 4.0));
sequence.append(owner.do_global_move(Vector2::new(124., 256.));
sequence.insert(8.0, owner.do_scale(Vector2::new(10., 10.));

// SpireSequence is just an alias for SpireTween<Sequence>, which means you can also nest them 
let nested_sequence = {
	let mut nested = SpireSequence::new();
	nested.append(owner.do_color(Color::BLUE, 2.0));
	nested.append(owner.do_move_y(Vector2::new(0, 69));
	nested
};

sequence.append(nested_sequence);

let handle = sequence.register();

需要终止一个缓动吗?

let handle: SpireHandle<DelayedCall> = ..;
handle.kill();

// As you may have guessed from the types, sequences are also tweens:
let sequence_handle: SpireHandle<Sequence> = ..;
sequence_handle.kill();

需要立即完成一个缓动吗?

handle.complete();

知道...

  • spire_tween不会以任何方式与Godot的内置缓动交互,它在其_process(f64)_physics_process(f64)虚拟方法上运行的独立自动加载中处理注册的缓动。

设置

设置过程假设您已经熟悉GDExtension的工作方式。

步骤 1

spire_tween添加到您项目的依赖项中:运行cargo add spire_tween或将其添加到您的Cargo.toml中:[dependencies] spire_tween = "0.1"

您很可能会想要使用最新版本,因为它将包含最新的错误修复。

步骤 2

一旦您构建了您的动态库,类TweensController应该在下一次您打开编辑器时自动与Godot注册。创建一个根名为“tweens_controller”的空场景,输入TweensController,然后将其添加为您的自动加载。

警告!节点名称必须为“tweens_controller”,否则每当尝试注册缓动时,Rust都会崩溃,导致您的游戏崩溃。

规则

不要尝试同时从两个地方借用TweensController,如果以下情况发生,则会出现这种情况:

  • 从以下地方注册缓动:在on_finish提供的回调内部或触发在您的do_propertydo_method调用之一中
  • 使用句柄从以下地方访问缓动:在on_finish提供的回调内部或触发在您的do_propertydo_method调用之一中
  • 您从不同的线程注册/访问缓动

帮助

在GitHub上打开一个问题或给我发消息:houtamelo

依赖关系

~7.5MB
~184K SLoC