Godot 免费跨平台游戏引擎 (四、脚本GDScript)
网络应用不可或缺,所以我有选择的跳到网络部份。
初始化网络
作为服务器初始化
var peer = NetworkedMultiplayerENet.new()
peer.create_server(SERVER_PORT, MAX_PLAYERS)
get_tree().set_network_peer(peer)
作为客户端初始化
var peer = NetworkedMultiplayerENet.new()
peer.create_client(SERVER_IP, SERVER_PORT)
get_tree().set_network_peer(peer)
获取先前设置的网络对等方
get_tree().get_network_peer()
检查是否被初始化为服务器或客户端
get_tree().is_network_server()
停止联网功能
get_tree().set_network_peer(null)
管理连接
服务器和客户端均有以下信号:
network_peer_connected(int id)
network_peer_disconnected(int id)
可以通过 SceneTree.get_network_unique_id() 获取网络连接的ID
客户端有以下信号:
connected_to_server
connection_failed
server_disconnected
RPC
RPC 远程过程调用,实现节点为之间的通信
rpc(“函数名”, <可选参数>)
rpc_id(<节点ID>,”函数名”, <可选参数>)
rpc_unreliable(“函数名”, <可选参数>)
rpc_unreliable_id(<节点ID>, “函数名”, <可选参数>)
同步成员变量
rset(“变量”, 值)
rset_id(<节点ID>, “变量”, 值)
rset_unreliable(“变量”, 值)
rset_unreliable_id(<节点ID>, “变量”, 值)
回到大厅
看看它的示例代码
extends Node
# 连接相关函数
func _ready():
get_tree().connect("network_peer_connected", self, "_player_connected") #玩家连接事件
get_tree().connect("network_peer_disconnected", self, "_player_disconnected") #玩家禁止连接事件
get_tree().connect("connected_to_server", self, "_connected_ok") #连接成功事件
get_tree().connect("connection_failed", self, "_connected_fail") #连接失败事件
get_tree().connect("server_disconnected", self, "_server_disconnected") #服务顺禁止连接
var player_info = {}
var my_info = { name = "Johnson Magenta", favorite_color = Color8(255, 0, 255) }
func _player_connected(id):
rpc_id(id, "register_player", my_info) #当一个玩家连接时,发送我的信息给对方
func _player_disconnected(id):
player_info.erase(id) #删除此ID的玩家信息
func _connected_ok():
pass #当是客户端时调用,服务端时不需要。这里没用,不处理。
func _server_disconnected():
pass #服务端踢我;显示出错信息并终止
func _connected_fail():
pass #无法连接到服务器,终止
remote func register_player(info):
var id = get_tree().get_rpc_sender_id() #获取rpc发送者ID
player_info[id] = info #存储玩家信息
#调用其它功能
这里有一个没有涉及到的关键词 remote ,它让Godot知道这个函数可以从RPC调用。
可以通过四个不同的关键词指定如何通过RPC调用该函数:
remote
remotesync
master
puppet
remote 意味着 rpc() 调用将通过网络发送并远程执行。
remotesync 意味着 rpc() 调用将通过网络发送并远程执行,但也将在本地执行
开始游戏
游戏角色场景
remote func pre_configure_game():
var selfPeerID = get_tree().get_network_unique_id() #获取我的网络连接ID
# 载入某个场景(大厅)
var world = load(which_level).instance()
get_node("/root").add_child(world)
# 载入我的角色,设置名字为网络连接ID
var my_player = preload("res://player.tscn").instance()
my_player.set_name(str(selfPeerID))
my_player.set_network_master(selfPeerID) # 以后再解释这是个啥
get_node("/root/world/players").add_child(my_player) #将我的角色加载到玩家节点
# 加载其它玩家
for p in player_info:
var player = preload("res://player.tscn").instance()
player.set_name(str(p))
player.set_network_master(p) # Will be explained later
get_node("/root/world/players").add_child(player)
# 告诉服务器,我方已连接完毕。(服务器的连接ID始终是1)
rpc_id(1, "done_preconfiguring", selfPeerID)
同步游戏开始
因为每个玩这的硬件和网络的差异,需确保所有人都加载完后,再统一开始游戏:
remote func pre_configure_game():
get_tree().set_pause(true) # 暂停
# 其余代码与上一节点的代码相同
当服务器从所有客户端获得OK信息时,才告诉所有的玩家开始:
var players_done = []
remote func done_preconfiguring(who):
# 完成配置通知,这里需要做一些必要检查:
assert(get_tree().is_network_server()) # 已作好初始化
assert(who in player_info) # 是否存在此玩家
assert(not who in players_done) # 没有包含信息在“已加载完成”变量中
players_done.append(who) # 将信息加入“已完成玩家”变量中
if players_done.size() == player_info.size(): # 加载完成所有玩家
rpc("post_configure_game")
remote func post_configure_game():
get_tree().set_pause(false) # 取消暂停
# 游戏开始了
同步游戏
在大多数游戏中,多人网络的目标是游戏在所有玩它的玩家身上同步运行。除了提供RPC和远程成员变量集实现之外,Godot还添加了网络主节点的概念。
象局域网战斗就存在主节点的概念。多人网络游戏中,服务器就是网络主节点。
网络主人
一个节点的网络主人是对该节点具有终极权限的客户端。
之前的示例中 my_player.set_network_master(selfPeerID) ,设置了网络主人是自己,而不是服务器。
这里有一个多人炸弹游戏来实例讲解。
主人和傀儡
??? 暂时跳过
HTTP请求
用 HTTPRequest 节点发出HTTP请求是最简单的方法。 它继承自更低级别的 HTTPClient 。
extends CanvasLayer
func _ready():
$HTTPRequest.connect("request_completed", self, "_on_request_completed")
func _on_Button_pressed():
$HTTPRequest.request("http://www.mocky.io/v2/5185415ba171ea3a00704eed")
func _on_request_completed(result, response_code, headers, body):
var json = JSON.parse(body.get_string_from_utf8())
print(json.result)
也可以定义header,类似 $HTTPRequest.request(“http://www.mocky.io/v2/5185415ba171ea3a00704eed", [“user-agent: YourCustomUserAgent”])
将数据发送到服务器
func _make_post_request(url, data_to_send, use_ssl):
var query = JSON.print(data_to_send)
var headers = ["Content-Type: application/json"]
$HTTPRequest.request(url, headers, use_ssl, HTTPClient.METHOD_POST, query)
在发送另一个请求之前,您必须等待请求完成。 一次发出多个请求需要每个请求有一个节点。 常见的策略是在运行时根据需要创建和删除HTTPRequest节点。
HTTP客户端类
这只是一个脚本,可以通过命令行来运行 godot -s http_test.gd
extends SceneTree
func _init():
var err = 0
var http = HTTPClient.new() # 创建客户端
err = http.connect_to_host("www.php.net", 80) # 连接
assert(err == OK) # 确定连接状态为OK
# 等待连接
while http.get_status() == HTTPClient.STATUS_CONNECTING or http.get_status() == HTTPClient.STATUS_RESOLVING:
http.poll()
print("Connecting...")
OS.delay_msec(500)
assert(http.get_status() == HTTPClient.STATUS_CONNECTED) # 不能连接
var headers = [
"User-Agent: Pirulo/1.0 (Godot)",
"Accept: */*"
]
err = http.request(HTTPClient.METHOD_GET, "/ChangeLog-5.php", headers) # 请求一个页面
assert(err == OK) # 确定连接状态为OK
while http.get_status() == HTTPClient.STATUS_REQUESTING:
# 只要请求被处理,就保持轮询
http.poll()
print("Requesting...")
if not OS.has_feature("web"):
OS.delay_msec(500)
else:
# web上不支持同步HTTP请求,因此请等待下一次主循环迭代。
yield(Engine.get_main_loop(), "idle_frame")
assert(http.get_status() == HTTPClient.STATUS_BODY or http.get_status() == HTTPClient.STATUS_CONNECTED) # 请求完成
print("response? ", http.has_response()) # 站点没有响应
if http.has_response():
# 如果有响应...
headers = http.get_response_headers_as_dictionary() # 获取响应头.
print("code: ", http.get_response_code()) # 显示响应代码
print("**headers:\\n", headers) # 显示headers
# 获取HTTP主体
if http.is_response_chunked():
# 它用块吗?
print("Response is Chunked!")
else:
# 或者只是简单的内容长度
var bl = http.get_response_body_length()
print("Response Length: ",bl)
# 不管怎样,这个方法对这两个都有效
var rb = PoolByteArray() # 将保存数据的数组。
while http.get_status() == HTTPClient.STATUS_BODY:
# While there is body left to be read
http.poll()
var chunk = http.read_response_body_chunk() # 获取一块.
if chunk.size() == 0:
# 什么都没有,等缓冲区填满一点。
OS.delay_usec(1000)
else:
rb = rb + chunk # Append to read buffer.
# 完成
print("bytes got: ", rb.size())
var text = rb.get_string_from_ascii()
print("Text: ", text)
quit()
SSL证书
通常希望使用SSL连接进行通信,这可以避免“中间人”攻击。 Godot有一个连接包装器 StreamPeerSSL ,它可以进行更安全的连接。 HTTPClient 类也通过使用相同的包装器来支持HTTPS。
为了使SSL工作,需要提供证书。必须在项目设置中指定.crt文件:
请记住将.crt添加为过滤器,以便导出器在导出项目时识别这一点。
WebSocket
WebSocket在Godot中通过三个主要类WebSocketClient、WebSocketServer和WebSocketPeer实现。
最小客户端示例
本例将向您展示如何创建到远程服务器的WebSocket连接,以及如何发送和接收数据。
extends Node
export var websocket_url = "ws://echo.websocket.org" # 连接的地址
var _client = WebSocketClient.new() # 实例化WebSocket客户端
func _ready():
# 连接基本信号以获得连接打开、关闭和错误的通知。
_client.connect("connection_closed", self, "_closed")
_client.connect("connection_error", self, "_closed")
_client.connect("connection_established", self, "_connected")
# 每次收到完整的数据包时,如果不使用多人API,就会发出此信号。
# 或者你可以循环检查 get_peer(1).get_available_packets()
_client.connect("data_received", self, "_on_data")
# 连接
var err = _client.connect_to_url(websocket_url)
if err != OK:
print("Unable to connect")
set_process(false)
func _closed(was_clean = false):
# was_clean将告诉您在关闭套接字之前,远程对等方是否正确通知了断开连接。
print("Closed, clean: ", was_clean)
set_process(false)
func _connected(proto = ""):
# proto 是WebSocket的可选子协议
print("Connected with protocol: ", proto)
# 发送数据
_client.get_peer(1).put_packet("Test packet".to_utf8())
func _on_data():
# 获取数据
print("Got data from server: ", _client.get_peer(1).get_packet().get_string_from_utf8())
func _process(delta):
# 在_process或_physics_process中。数据只有在调用以下函数时才会发送和接收。
_client.poll()
最小服务端示例
这个例子将向您展示如何创建一个WebSocket服务器来监听远程连接,以及如何发送和接收数据。
extends Node
const PORT = 9080 # 侦听端口
var _server = WebSocketServer.new() # 服务端实例化
func _ready():
# 相关事件
_server.connect("client_connected", self, "_connected")
_server.connect("client_disconnected", self, "_disconnected")
_server.connect("client_close_request", self, "_close_request")
# 接收数据,或者通过轮询 get_peer(PEER_ID).get_available_packets()
_server.connect("data_received", self, "_on_data")
# 启动监听
var err = _server.listen(PORT)
if err != OK:
print("Unable to start server")
set_process(false)
func _connected(id, proto):
# id是连接id,proto是子协议
print("Client %d connected with protocol: %s" % [id, proto])
func _close_request(id, code, reason):
# 返回有关闭的代码和原因
print("Client %d disconnecting with code: %d, reason: %s" % [id, code, reason])
func _disconnected(id, was_clean = false):
print("Client %d disconnected, clean: %s" % [id, str(was_clean)])
func _on_data(id):
var pkt = _server.get_peer(id).get_packet()
print("Got data from client %d: %s ... echoing" % [id, pkt.get_string_from_utf8()])
_server.get_peer(id).put_packet(pkt)
func _process(delta):
# 与上方一样,都是需要在process中调用此函数,才能收发信息
_server.poll()
WebRTC
名称源自网页即时通信(Web Real-Time Communication),是一个支持网页浏览器进行实时语音对话或视频对话的API。
WebRTC实现了基于网页的视频会议,标准是WHATWG 协议,目的是通过浏览器提供简单的javascript就可以达到实时通讯能力。
客户端分别与服务端通信,通过服务端允许客户端双方连接,然后客户端断开服务端连接,实现客户端相互通信。
Godot中通过WebRTCPeerConnection和WebRTCDataChannel两个主要类来实现,以及多人API实现webrtcdmultiplayer。
这些类在Html5中是自有的,但在本地平台上需要GDNative插件。
暂时用不上,继续挖坑,跳过先。
这是网上的一个例子: 聊天程序。很简单的一个实例。
extends Node2D
var debug_text
var state_text
var text_toSend
func _ready():
debug_text=$Label #用于显示发送过来的消息
state_text=$state #用于显示当前是服务端还是客户端
get_tree().multiplayer.connect("network_peer_packet",self,"_on_packet_received") #连接事件
func _on_packet_received(id,packet):
debug_text.text=packet.get_string_from_ascii() #把消息显示出来
#发送信息按钮
func _on_send_pressed():
var result=get_tree().multiplayer.send_bytes(text_toSend.to_ascii())
#debug_text.text=packet.get_string_from_ascii()
#服务器按钮(自己作服务器)
func _on_server_pressed():
NetWork.create_server() #创建服务器
state_text.text="server" #状态中显示我是服务器
#客户端按钮(自己作客户端)
func _on_client_pressed():
NetWork.create_client() #创建客户端
state_text.text="client" #显示状态
#编辑框输入
func _on_LineEdit_text_changed(new_text):
text_toSend=new_text
这个实例中,我们居然连IP和端口都没有管,直接就通上话了。当然 create_server 和 create_client 函数是可以指定IP和端口
不过即使我加载上中文字体,它依然没有在信息框显示收到的中文。