3个不稳定版本

0.5.0 2022年2月11日
0.4.1 2021年10月6日
0.4.0 2021年9月22日

#1273 in 游戏开发

MIT许可

56KB
866

godot-ggrs-wrapper

此仓库的目的是创建一个ggrs Rust包的包装器,以便可以在Godot游戏引擎中使用它。为此,使用了godot-rust GDNative绑定。尽管该项目目前实现了所有GGRS功能,但我无法保证它已准备好投入生产使用。

注意

  • 建议您在Godot资源GDNativeLibrary中将reloadable设置为false
  • 已在Godot版本3.3.2上测试,Godot 4.0将具有大量的GDNative功能,因此当它发布时,可以假设此项目将崩溃。
  • GodotGGRS的输入当前为无符号32位整数格式。
  • 状态为GodotByteArrays,您将godot变体转换为ByteArray并传递给GodotGGRS。
  • 编译需要Clang,详细信息请参见此处

示例

使用godot-ggrs-wrapper的项目

如果你的项目使用了godot-ggrs-wrapper,请告诉我,我将很高兴添加链接!

快速开始

由于此项目使用godot-rust GDNative绑定,因此你应该熟悉如何在Godot中设置godot-rust GDNative节点。如果你不熟悉,请查看此页面以获取详细信息。你想要的设置是一个场景,其中包含一个绑定到GodotGGRSP2PSession类的节点。

使用GodotGGRSP2PSession启动会话

要设置会话,你想要一个脚本,它解决了GGRS节点。仅仅在场景中有一个节点并不能为你创建一个会话。在这里,我使用了根节点,创建了一个在_ready()函数上创建会话的脚本

func _ready():
	var local_handle: int #This var is later used to identify your local inputs
	var remote_handle: int #This var is later used to identify the remote_players inputs
	if(OS.get_cmdline_args()[0] == "listen"):
		$GodotGGRS.create_new_session(7070, 2) # Port 7070, 2 max players, max 8 prediction frames
		local_handle = $GodotGGRS.add_local_player()
		remote_handle = $GodotGGRS.add_remote_player("127.0.0.1:7071")
	elif(OS.get_cmdline_args()[0] == "join"):
		$GodotGGRS.create_new_session(7071, 2) # Port 7071, 2 max players, max 8 prediction frames
		remote_handle = $GodotGGRS.add_remote_player("127.0.0.1:7070")
		local_handle = $GodotGGRS.add_local_player()

	$GodotGGRS.receive_callback_node(self) # Set the node which will implement the callback methods
	$GodotGGRS.set_frame_delay(2, local_handle) # Set personal frame_delay, works only for local_handles.
	$GodotGGRS.start_session() #Start listening for a session.

如你所见,我们根据谁是“主机”交换添加玩家的顺序。在现实中,由于它是一个对等库,没有真正的主机。但是你应该有一种方法来区分玩家1和玩家2。

推进帧

现在我们已经有了会话,我们想要开始实现循环。Godot 的默认 _process()_physics_process() 在这里会很好地为我们服务。

func _process(_delta):
	$GodotGGRS.poll_remote_clients() # GGRS needs to periodically process UDP requests and such, sticking it in \_process() works nicely since it's only called on idle.

func _physics_process(_delta):
	if($GodotGGRS.is_running()): # This will return true when all players and spectators have joined and have been synched.
		$GodotGGRS.advance_frame(local_handle, raw_input_to_int("con1")) # raw_input_to_int is a method that parses InputActions that start with "con1" into a integer.

func raw_input_to_int(prefix: String)->int:
	# This method is how i parse InputActions into an int, but as long as it's an int it doesn't matter how it's parsed.
	var result := 0;
	if(Input.is_action_pressed(prefix + "_left")): #The action it checks here would be "con1_left" if the prefix is set to "con1"
		result |= 1
	if(Input.is_action_pressed(prefix + "_right")):
		result |= 2
	if(Input.is_action_pressed(prefix + "_up")):
		result |= 4
	if(Input.is_action_pressed(prefix + "_down")):
		result |= 8
	return result;

调用 advance_frame 将会告诉 GGRS 你已经准备好使用作为参数提供的输入进入下一帧。GGRS 会完成它的工作,并在准备好继续时回调 Godot。

处理 GGRS 回调

因此,如何处理 GGRS 回调比之前的步骤更加主观,并且会根据你的游戏构建方式有很大差异。唯一需要的是实现回调函数,但内部的逻辑可以是几乎任何东西,以适应你的游戏。以下是如何实现回调方法的方式。

func ggrs_advance_frame(inputs: Array):
	# inputs is an array of input data indexed by handle.
	# input_data itself is also an array with the following: [frame: int, size: int, inputs: int]
	# frame can be used as a sanity check, size is used internally to properly slice the buffer of bytes and inputs is the int we created in our previous step.
	var net1_inputs := 0;
	var net2_inputs := 0;
	if(local_handle < remote_handle):
		net1_inputs = inputs[local_handle][2]
		net2_inputs = inputs[remote_handle][2]
	else:
		net1_inputs = inputs[remote_handle][2]
		net2_inputs = inputs[local_handle][2]
	int_to_raw_input("net1", net1_inputs) # Player objects check for InputActions that aren't bound to any controller.
	int_to_raw_input("net2", net2_inputs) # Player objects check for InputActions that aren't bound to any controller.
	_handle_player_frames()

func ggrs_load_game_state(frame: int, buffer: PoolByteArray, checksum: int):
	var state : Dictionary = bytes2var(buffer);
	P1.load_state(state.get("P1", {}))
	P2.load_state(state.get("P2", {}))

func ggrs_save_game_state(frame: int)->PoolByteArray: # frame parameter can be used as a sanity check (making sure it matches your internal frame counter).
	var save_state = {}
	save_state["P1"] = _save_P1_state("");
	save_state["P2"] = _save_P2_state("");
	return var2bytes(save_state);

func int_to_raw_input(prefix: String, inputs: int):
	_set_action(prefix + "_left", inputs & 1)
	_set_action(prefix + "_right", inputs & 2)
	_set_action(prefix + "_up", inputs & 4)
	_set_action(prefix + "_down", inputs & 8)

func _set_action(action: String, pressed: bool):
	if(pressed):
		Input.action_press(action)
	else:
		Input.action_release(action)

处理 Rust Panic

创建一个包含以下内容的 Godot 脚本

extends Node

func rust_panic_hook(error_msg: String) -> void:
    assert(false, error_msg)

使用以下名称将此脚本作为单例自动加载到 Godot 项目中:"RustPanicHook"。现在所有 Rust panic 都应该被正确捕获。这个解决方案基于 Godot-Rust - Rust Panic Hook Recipe

依赖项

~5–8.5MB
~157K SLoC