前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Godot游戏开发实践之一:使用High Level Multiplayer API制作多人游戏(上)

Godot游戏开发实践之一:使用High Level Multiplayer API制作多人游戏(上)

原创
作者头像
IT自学不成才
修改2020-08-03 10:56:18
1.7K0
修改2020-08-03 10:56:18
举报
Godot游戏开发实践之一
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 局域网多人游戏开发应用undefined阅读时间: 10 分钟undefined永久链接: http://liuqingwen.me/2020/07/22/godot-game-devLog-1-making-game-with-high-level-multiplayer-api-part-1/undefined系列主页: http://liuqingwen.me/introduction-of-godot-series/

二、正文

demo12.jpg
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 中可以使用下面的代码:

代码语言:txt
复制
if self.get_tree().is_network_server():
    print('this is the server.') # 服务器端
else:
    print('this is the client.') # 客户端

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

代码语言:txt
复制
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所在场景,右边反映的是另一个玩家所在游戏场景:

undefined(https://upload-images.jianshu.io/upload_images/4470535-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(镜像)都会同时发出攻击动作,代码如下:

代码语言:txt
复制
func _input(e : Event) -> void:
    # 只会在本地运行(玩家A)
    attack()
    # 可以调用远程方法(玩家A的所有镜像)
    rpc('attack')

# remote 表示该方法可以被远程调用
remote func attack() -> void:
    print('attack something...')

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

代码语言:txt
复制
# 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场景结构

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

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“同步”爆炸,在本地也需要手动调用一次普通方法:

代码语言:txt
复制
# 玩家A中的“炸弹K”,使用 rpc 调用远程爆炸方法
self.rpc('_deleteObject')
# 本地调用:本身也需要调用一次该方法
_deleteObject()

# 通用方法:玩家A/B/C中的:“炸弹K”
remote func _deleteObject() -> void:
    print('Explode and delete self.')

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

代码语言:txt
复制
# rpc 远程调用,因为是 remotesync 修饰所以本身也会调用一次
self.rpc('_deleteObject')

# 使用 remotesync 表示该方法调用时本地也会触发
remotesync func _deleteObject() -> void:
    print('Explode and delete self.')

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

代码语言:txt
复制
# 炸弹触发爆炸事件后所调用的一个方法
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 关键字保证了方法或者属性只能在“奴隶”节点上发生调用:

代码语言:txt
复制
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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、正文
    • 多人游戏开发简介
      • 远程调用基础
        • 远程调用关键字
          • 游戏结构
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档