美文网首页Godot 游戏引擎
Godot游戏开发实践之一:使用High Level Multi

Godot游戏开发实践之一:使用High Level Multi

作者: spkingr | 来源:发表于2020-07-24 17:46 被阅读0次
    Godot游戏开发实践之一

    一、前言

    距离上一次发文已经稳稳超过一年了,去年一直在做 #¥@#*!%……%#&…%&^# 然后待在家里了!偶尔写写 BUG ,一直默默关注着 Godot ,这不已经 3.2.2 版本了,距离“神秘”的 4.0 版本又近了一步。接下来我还是会不断探索,努力提高自己,努力提高别人,哈哈。有时间多和大家交流探讨 Godot 游戏开发中的一些技能、技巧、技术吧。 :sunglasses:

    该结束了!我说的是往期的 Godot3 游戏引擎入门系列正是宣布完成,我们不能总是停留在入门阶段,不要局限于写小 Bug ,大 Boss 也得搞搞,我打算邀请大家一起进入下一阶段的深入学习,本人斗胆提了个高大上的名字: Godot 游戏开发实践系列。说白了,就是“踩坑填坑”系列,至于内容,我暂时能想到、能做到的只有以下一点东西:

    • Godot 的开发技巧、高级 API 的探索
    • Shader 着色器入门和应用
    • AI 的一些入门级应用学习
    • 继续实践,做不同类型的游戏 Demo
    • 赶在 4.0 之前入个 3D 游戏开发的门
    • 其他,或者资源,还有太多没学到的……

    我也是新手,很多内容都是第一次尝试,不过不要紧,有梁静茹给的“勇气”,希望“我的一小步,让大家前进一大步吧!”哈哈。另外,喜欢 Godot 游戏引起的朋友们,强烈推荐入群交流, QQ 群号: 692537383 ,和我上次推荐的不是一个群,该群群主是 Godot 第三方语言 QuickJS 绑定者,技术大牛,而且群里的学习讨论、交流气氛也不错,记得在入群申请的时候报上我的名字,进群后可以享受“发际线高端维护优惠券”一张还有群主香吻一个! :joy: 不谢!(PS: 另有新群 831931065 也推荐加入。

    主要内容: High Level Multiplayer API 局域网多人游戏开发应用
    阅读时间: 10 分钟
    永久链接: http://liuqingwen.me/2020/07/22/godot-game-devLog-1-making-game-with-high-level-multiplayer-api-part-1/
    系列主页: http://liuqingwen.me/introduction-of-godot-series/

    二、正文

    demo12.jpg

    本次示例是一个局域网联机小游戏:炸弹人,当然不能直接在网上进行联机,我还没写过任何服务器代码,不过有一个平台支持 Godot 的局域网游戏进行“网络联机”,并能邀请他人一起玩: gotm.io ,想试一下这个游戏的朋友,这里有体验链接: https://gotm.io/spkingr/bomberman ,进入游戏后,创建服务器,然后网页的右下角有个邀请链接,复制后发送给朋友就可以一起痛苦地玩耍了。由于服务器在国外,要想不卡,对网速要求是比较高的。关于 Godot 中局域网游戏开发可以参考官方文档教程:High-level multiplayer ,文档内容有点简洁,本着“填坑”的思想,我把开发过程中遇到的一些问题和解决方案记录下来,这也是本篇文章的出发点,大致内容:

    1. 局域网多人联网游戏开发介绍
    2. 远程调用基础知识
    3. Godot 中几个重要的关键字
    4. 游戏结构、代码简析
    5. 经验总结

    示例源码我已经上传到 Github 并且被打包运往北极,妈妈再也不担心我的“祖传代码”会被弄丢了!哈哈。 :joy:

    多人游戏开发简介

    多人游戏开发听上去感觉要比单机游戏开发高端,实际上并不复杂,只要了解多人游戏开发中的几个重要概念,开发起来和单人游戏几乎没啥区别。在多人游戏中,有一个重要的概念是区分:服务端和客户端。在一场局域网联机游戏中,有一个玩家是服务器,即 Server ,其他加入的玩家都是 Client 客户端,在游戏开发代码编写上,它们几乎“平等”:

    • 服务端和客户端共享相同的场景和代码
    • 都可以互相调用远程方法,发送通知等
    • 也可以独立运行相关逻辑,比如初始化一些共有的数据
    服务器和客户端场景结构图对比

    上图显示的是服务器端和客户端的场景图,节点和结构完全一样,当然也共享同一套代码,不过我们知道,在运行过程中不可能让客户端随意、单独、自定义地运行任何代码,那样的话游戏就不能保持进度同步了,多人游戏也就成了单机游戏。相比客户端,服务端至少拥有以下特殊职能:

    1. 服务端优先于其他客户端先运行、创建游戏实例
    2. 服务端负责统一分配某些属性值,比如给玩家随机分配颜色,确保不重复
    3. 服务端可以踢人,可以通知并开始游戏,客户端一般不具有该功能
    4. 服务端一般不会随便退出正在进行中的游戏,至少也要发送一个通知或者提示

    如何在代码中判断当前游戏是否为服务器非常简单,在 Godot 中可以使用下面的代码:

    if self.get_tree().is_network_server():
        print('this is the server.') # 服务器端
    else:
        print('this is the client.') # 客户端
    

    在这个 Demo 中,所有的“怪物”都在服务器端产生,然后“同时通知所有其他客户端生成相同属性的敌人”:

    func _spawnEnemies() -> void:
        # 只有服务端可以控制敌人对象的生成
        if ! self.get_tree().is_network_server():
            return
    
        var count := _enemiesContainer.get_child_count()
        if count <= maxEnemyCount:
            _spawnEnemy() # 生成怪物
    

    逻辑很简单,那么服务端如何通知客户端怪物对象的生成呢?换句哈说,也就是服务端如何在运行时发送消息到客户端,消息内容包括客户端需要生成怪物的位置、名字、状态等变量值,这就需要高大上且专业的远程调用相关 API 了:低端点,就是远程方法调用的实现。在 Godot 中我们使用 rpc 关键字调用远程方法, rset 调用远程属性,了解了服务器和客户端,接下来一起深入探讨远程调用相关知识。

    远程调用基础

    前方预警:各种七嘴八舌、鱼龙混杂、绕口令式的句段可能会让小白们感觉不适,慎读!莫晕!勿醉!

    何谓远程调用?有点网络知识的朋友都知道,所谓“远程”就是本地与非本地,或者联网中的服务端、客户端之间的关系,举一个很简单的例子:玩家A玩家B联网游戏,玩家A发送一条消息后,这条消息会同时显示在两个玩家的屏幕上,玩家A的消息就是通过远程调用传送到玩家B的游戏场景进行显示的。

    再举个例子:玩家A进入多人游戏场景,那么服务器端和客户端都有玩家A对象,但实际上只有一个地方(比如服务端)可以操作控制自己的角色,比如玩家A在服务器端通过键盘事件控制位置移动后,客户端几乎同时也能看到玩家A移动到了相同的某个新位置,这个流程就是一个简单的远程调用实现过程。具体点,就是服务端接收键盘输入,玩家移动后,通过远程调用客户端相应方法,让客户端实现移动该场景中的玩家A(傀儡/镜像),这个所谓的傀儡有个专业名词叫奴隶( slave )或者木偶 ( puppet )。有点啰嗦,用一个简单的动态图演示如下,注意左边是受控制的真实玩家A所在场景,右边反映的是另一个玩家所在游戏场景:

    ![远程调用移动](https://img.haomeiwen.com/i4470535/78795823ae49ff74.gif?imageMogr2/auto-orient/strip

    对于小白来说,了解了这个过程就是理解了这个游戏的核心部分。在 Godot 中,除了 rpc/rset 关键字外,还有几个关键字。还是用例子来说:假设三个玩家联网玩游戏,玩家A/B/C在紧张刺激地进行游戏,这里他们各自控制自己的主角,我们把他们各自打开的游戏界面或场景定义为各自所谓的主场景。某个时候玩家A在自己的主场景中发送了一条私密信息,这条信息以玩家C为特定的接收对象,也就是说玩家B所在场景是看不到该消息的,只有玩家C才能看到,如何实现呢?这就是有选择性、定向性的远程调用了,是通过一个 network id 实现的。游戏联网后,每个玩家(服务器、客户端)都有一个特定的网络 id (在前面的场景结构图中,两个玩家 1 和 62889 实际就是他们各自的 ID ),通过这个 id 利用 rpc_id 或者 rset_id 方法就可以向指定端发送私密信息了。

    说明:服务器端 ID = 1 ,其他客户端 ID 都是随机数。

    例子到此为止,在 Godot 中远程调用 API 有以下几个,这些都是 Node 节点自带的方法:

    • rpc/rset 调用远程方法或者属性
    • rpc_id/rset_id 调用指定 id 对象的远程方法或者属性
    • rpc_unreliable/rset_unreliable 和上面类似,但不保证一定会调用,可能因为延迟等原因掉包
    • rpc_unreliable_id/rset_unreliable_id 和上面类似,针对指定 id 的不稳定远程调用

    "talk is cheap, show me the code!" 多人游戏中,服务端有“玩家A”和“玩家B(镜像)”,客户端同样有“玩家A(镜像)”和“玩家B”,当服务器端玩家A(客户端的玩家B同理)按下“攻击”按键的时候,服务端的玩家A和客户端的玩家A(镜像)都会同时发出攻击动作,代码如下:

    func _input(e : Event) -> void:
        # 只会在本地运行(玩家A)
        attack()
        # 可以调用远程方法(玩家A的所有镜像)
        rpc('attack')
    
    # remote 表示该方法可以被远程调用
    remote func attack() -> void:
        print('attack something...')
    

    同理,远程属性的调用代码示例:

    # remote 表示该属性可以被远程调用
    remote var health := 100
    
    func damage(value : int) -> void:
        self.health -= value
        rset('health', self.health)
    

    大家应该注意到了,有的方法、属性的定义前多了一个关键字 remote ,正如单词的意思,这个关键字修饰的方法/属性不同于普通方法/属性:能使用 rpc/rset 进行远程调用。

    除此之外,细心的朋友能发现,在上面的 GIF 演示图中还有两个关键字: master/puppet 。这两个关键字并不是玩家的名字(因为他们不同),同样是远程调用中的关键字,分别代表该节点为当前场景的“主节点”或者“奴隶(傀儡、木偶、镜像)节点”。而普通方法前除了可以用 remote 修饰外,也可以使用 master/puppet 修饰,接下来重点讨论这些关键字的意义和应用。

    远程调用关键字

    为了把主/奴区分开来,我还是继续举例子,假设联机玩家A/B/C在各自电脑上的各自场景中一起游戏(果然 RAP ),那么下面的高深结论成立:

    • 相对于玩家A来说:玩家B和玩家C都属于远程端(他们三个有一个服务端,两个客户端)
    • 相对于玩家A电脑中的场景:玩家A对象是主人节点,玩家B和玩家C是对应的奴隶节点
    • 同理,相对于玩家B中的场景:玩家B对象是主人节点,A和C都是奴隶节点
    • 玩家A只能是玩家A的主人节点或者奴隶节点,不可能玩家A的主人节点或者奴隶节点是玩家B/C
    • 比如:玩家A场景中的A对象是玩家B场景中A对象的主人节点,玩家B/C场景中A也是玩家A场景中A对象的奴隶节点( RAP 唱起来! )

    不管你有没有搞懂,反正我是没办法再举例子了。太混乱了!小二,来瓶 80 年的 XO 压压惊……“酒醒后第二天,发现下图能看懂了!”

    master和puppet场景结构

    上图说明两个联机游戏场景的结构是完全一样的,但有“主次”节点之分,在实际游戏中的就像下图:

    master和puppet在场景中的节点

    总结一下,在 Godot 中用于修饰远程属性/方法的几个主要关键字就这几个:

    • remote 表示该方法是一个远程方法或者属性,可以使用 rpc/rset 调用
    • remotesync 以前写作 sync ,它不仅会调用远程方法,也会在本地调用一次
    • master 表示该方法只能在“主人”节点中调用,“奴隶”节点不会调用
    • puppet 以前写作 slave ,和 master 相反,在所有“奴隶”身份节点中调用

    "talk is cheap, show me the code!" 为了区分 remote/remotesync 关键字,再举个栗子,我发誓这最后一个 RAP :假设“炸弹K”所在的场景,调用了一个“爆炸然后消失”的远程方法,因此其他场景中,不论服务器端还是客户端的“炸弹K”镜像都会“爆炸然后消失”。但问题来了,“炸弹K”本身并没有爆炸,为啥?因为这里调用的是远程方法,本地方法并没有调用,所以,为了保证游戏中炸弹K“同步”爆炸,在本地也需要手动调用一次普通方法:

    # 玩家A中的“炸弹K”,使用 rpc 调用远程爆炸方法
    self.rpc('_deleteObject')
    # 本地调用:本身也需要调用一次该方法
    _deleteObject()
    
    # 通用方法:玩家A/B/C中的:“炸弹K”
    remote func _deleteObject() -> void:
        print('Explode and delete self.')
    

    上面的代码显然有点啰嗦,我们改用 remotesync 可以让代码稍许简洁:

    # rpc 远程调用,因为是 remotesync 修饰所以本身也会调用一次
    self.rpc('_deleteObject')
    
    # 使用 remotesync 表示该方法调用时本地也会触发
    remotesync func _deleteObject() -> void:
        print('Explode and delete self.')
    

    实际上, remote 完全可以替代 remotesync ,视具体情而定吧,像类似上述的场景中 remotesync 更加方便。另一方面, masterpuppet 也具有类似的特点,同样表示远程属性或者方法,不过他们明确了调用者的“身份”,比如游戏中的一段代码:

    # 炸弹触发爆炸事件后所调用的一个方法
    func _on_Explosion_body_entered(body : CollisionObject2D) -> void:
        if body != null && body.has_method('bomb'):
            # 调用 body 的 bomb 方法,这里 bomb 方法只有主人节点才会发生实际调用
            body.rpc('bomb')
            self.queue_free()
    
    # 玩家场景中的代码,使用 master 表示远程调用中只有“主人节点”会触发
    master func bomb() -> void:
        print('Damaged by bomb.')
        _isStunning = true
        stun()
        # 主人节点使用远程调用通知所有其他奴隶节点
        self.rpc('stun')
    
    # 这里当然可以改为 remotesync 或者 puppet
    remote func stun() -> void:
        print('stunning...')
    

    相同的道理, puppet 关键字保证了方法或者属性只能在“奴隶”节点上发生调用:

    func _physics_process(delta):
        # 这里对当前节点进行判断:非主人节点则返回
        if ! self.is_network_master():
            return
    
        if _isStuning || _isDead:
            return
    
        # 主人节点根据键盘输入移动位置
        self.move_and_slide(_velocity)
    
        # 因为奴隶节点不接受键盘输入的控制,所以必须由主人节点远程控制移动
        self.rpc_unreliable('_updatePosition', self.position)
    
    # 这个方法只会在奴隶节点中调用(依然可以改为 remote )
    puppet func _updatePosition(pos : Vector2) -> void:
        self.position = pos
    

    在源码中,你会发现很多方法中都包含 Node.is_network_master() 的判断语句,这是为了避免该方法在非“主人”节点中运行。值得注意的是,这个方法和 Node.get_tree().is_network_server() 是完全不相干的两种判断,前者表示当前节点是否为主人节点,是任何 Node 节点具有的一个方法;后者表示当前游戏是否为服务器,是场景树 Tree 的一个方法。

    写了这么多,说了那么多 RAP ,也举了不少例子,对于编写过服务器代码的朋友来说应该不难,作为新手还是需要一些思考和实践的,现在,总结一下前面的内容:

    方法(属性) 本地节点是否运行 远程节点是否运行 本地主节点是否运行 本地奴隶节点是否运行
    普通法法
    remote
    remotesync
    master 是/否(视情况) 是/否(视情况)
    puppet 是/否(视情况) 是/否(视情况)

    完成了这个游戏后,我发现:本质上来说,我们完全只需要一个 remote 结合 is_network_master() 方法就可以实现其他所有关键字的功能,因为在 remote 方法中完全可以判断当前节点是否为主人节点还是奴隶节点。当然,那样会很麻烦,合理且灵活地应用每个修饰符,能够写出更加简洁、易读的代码。

    另外的另外,还有几个关键字,比如 mastersync/puppetsync 我没有在游戏中用到,大家可以到官方文档中进行查询了解,接下来我们一起讨论本 Demo 中的场景结构和相关代码吧。

    游戏结构

    限于篇幅过长,我将在下部分再详述,尽情期待! :smiley:

    未完待续……

    我的博客地址: http://liuqingwen.me ,我的博客即将同步至腾讯云+社区,邀请大家一同入驻: https://cloud.tencent.com/developer/support-plan?invite_code=3sg12o13bvwgc ,欢迎关注我的微信公众号:

    IT自学不成才

    相关文章

      网友评论

        本文标题:Godot游戏开发实践之一:使用High Level Multi

        本文链接:https://www.haomeiwen.com/subject/molslktx.html