(原) Godot 免费跨平台游戏引擎(八、网络)

原创文章,请后转载,并注明出处。

Godot 免费跨平台游戏引擎 (一、初见)

Godot 免费跨平台游戏引擎(二、第一个游戏)

Godot 免费跨平台游戏引擎(三、理论是实践的基础)

Godot 免费跨平台游戏引擎 (四、脚本GDScript)

Godot 免费跨平台游戏引擎(五、常用英文)

Godot 免费跨平台游戏引擎(六、一些收集)

Godot 免费跨平台游戏引擎(七、2D)

Godot 免费跨平台游戏引擎(八、网络)

Godot 免费跨平台游戏引擎(九、GUI外观)

Godot 免费跨平台游戏引擎(十、相关资源)

Godot 免费跨平台游戏引擎(十一、源码编译)

Godot 免费跨平台游戏引擎(十二、软件GUI)

Godot简单制作残影效果

Godot ParallaxBackground 视差背景

Godot 使用Light2D实现遮罩效果

码农家的孩子:学字母(Godot改版中)


网络应用不可或缺,所以我有选择的跳到网络部份。

初始化网络

作为服务器初始化

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和端口

不过即使我加载上中文字体,它依然没有在信息框显示收到的中文。

相关文章