Godot3游戏引擎入门之十三:实现玩家的跳跃功能并完成一个平台小游戏(下)

2019-01-26 by Liuqingwen | Tags: Godot | Hits

一、前言

本文为上一篇文章的续篇,在上一节中我们一起讨论了 Godot 中平台游戏玩家跳跃功能的实现方法,并使用到了本次小游戏中,那么本节内容主要是分享这个平台小游戏的基本结构和制作过程。游戏运行效果如下:

效果图

游戏中的图片资源主要来源于 OpenGameArt.org 网站: https://opengameart.org/content/a-platformer-in-the-forest ,游戏中的部分音乐资源也是在该网站中找到的,其他部分图片和音乐是我七零八凑拼起来的,为了顺利完成一个小游戏,找资源也花费了我不少时间,在这里建议大家有时间可以学习一些基础的美术知识吧。

本次两篇文章所详细讲述的内容包括以下部分:

  1. 重力加速度知识和简单的跳跃实现方法(上)
  2. 二次跳跃的实现(上)
  3. 精确高度的跳跃实现(上)
  4. 游戏场景结构与主要代码
  5. 三种敌人的行为和实现
  6. 其他的一些效果介绍
  7. 问题和总结

主要内容:平台游戏的制作解析
阅读时间: 10 分钟
永久链接: http://liuqingwen.me/2019/01/26/introduction-of-godot-3-part-13-the-player-jump-implementation-and-make-a-platform-game-part-2/
系列主页: http://liuqingwen.me/introduction-of-godot-series/

二、正文

本篇目标

  1. 平台游戏中的玩家场景
  2. 游戏中的三种敌人实现
  3. 游戏中的其他部分简述
  4. 问题与总结

上一篇文中我们花了大篇幅探讨平台游戏的最核心部分:玩家跳跃功能的实现,接下来所要讨论的游戏中其他场景相对来说就非常简单了,特别是在结合本系列文章之前的两个小游戏的基础上,我觉得对于新手而言也不会有很大的难度,而游戏中的相关设计,额外小功能的实现,游戏场景的丰富程度等等这些还是需要自己好好打磨的。正因为如此,这些可以扩展、发挥的部分也就留给大家去思考实现啦,我仅仅是希望做到抛砖引玉的作用吧!

玩家场景

玩家还是那个玩家,节点还是那些节点。嗯,相比之前的游戏,玩家场景的结构变化不大,当然也有不同之处,玩家场景中最重要的变化是多了一个非常重要的节点: Camera2D 摄像机节点。这个节点其实非常好理解,特别是在 3D 游戏中更加常用,而且很多游戏中会同时拥有多个摄像机。

godot_13_camera_node.png

Camera2D 节点的一些基本参数很好理解,这里例举几个重要的参数说明如下:

  • Current 设置为当前摄像头,这个参数非常重要,不勾选摄像头将不起作用
  • Zoom 摄像机镜头的缩放变焦,值越小范围越小,图片的显示也就越大
  • Limit 摄像机的最大活动范围,摄像头移动后的位置不会超出该限制
  • Smoothing/Speed 开启摄像头跟随平滑移动,可以设置移动的速度
  • Drag Margin 摄像头不跟随移动的尺寸范围,超出该范围摄像头开始跟随运动
  • Editor 显示摄像机相关范围图形界面,不会在游戏中显示,用于调试

在属性面板中,你会发现 Limit 下的默认值设置的有点荒谬,当然这在一般的游戏中并没有什么问题,你也可以手动设置合理的值,把摄像头的最大活动范围固定在游戏的主场景范围内,另外 Editor 菜单中的几个调试工具建议都开启查看,参考上图,其中黄色区域表示摄像机活动范围,紫色表示屏幕尺寸或者视窗 Viewport 的大小,蓝色表示玩家在此范围内摄像机不会发生移动,超出范围摄像机跟随移动。

除此之外,我在摄像机节点下添加了一个带脚本的空节点,脚本代码简单地实现了摄像头抖动效果,实现原理非常简单,有兴趣的朋友可以下载源码看看。整个玩家的场景结构如下图:

godot_13_player_node.png

UI 场景结构图也展示在上面了,场景的根节点是一个 CanvasLayer 节点,它的特点是能够忽略节点的渲染排序,直接显示在所有层的最上层,它的子节点又包含 3 个 TextureRect 节点,用于显示玩家的血量。

在这里我建议:如果你的 UI 场景比较简单,完全可以把 UI 场景直接添加到玩家场景中作为一个直接子节点,更加方便简洁! smiley

敌人场景

这个平台游戏中,除了我们的主角——玩家子场景稍显复杂外,其他稍微复杂的节点就是敌人场景了,我在游戏中制作了 3 种行为各异的敌人:光头( NakedEnemy )、军人( SoldierEnemy )以及蛇( SnakeEnemy )。

godot_13_enemy_nodes.png

从场景的结构可以看出来,其中两个敌人的场景根节点使用的是 KinematicBody2D 节点,而怪物蛇使用的则是 Area2D 节点,它们具有的共同行为是:左右反复巡逻。而不同的行为特点有这么几个:

  • 怪物蛇只巡逻,没有主动攻击的能力,场景实现代码也是最简单的
  • 光头在巡逻过程中如果与玩家发生碰撞会立刻进行攻击,之后继续巡逻
  • 军人无主动攻击的能力,但会在巡逻时会往前方发射多颗子弹

怪物蛇 SnakeEnemy 场景的实现代码最简单,代码如下:

SnakeEnemy.gd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
extends Area2D

const UNIT = 16

export(float) var patrolRange = 1 * UNIT

onready var _sprite = $Sprite
onready var _animator = $AnimationPlayer

var _walkSpeed: float = 2 * UNIT / 2.0
var _startPosition: float = 0.0


func _ready():
# 设置开始位置,注意为全局位置
_startPosition = self.global_position.x


func _process(delta):
self.position.x += _walkSpeed * delta
if self.global_position.x >= _startPosition + patrolRange || self.global_position.x <= _startPosition - patrolRange:
_walkSpeed = - _walkSpeed
_sprite.flip_h = ! _sprite.flip_h


# 发生碰撞时,检测是否为玩家
func _on_SnakeEnemy_body_entered(body):
if body.is_in_group('player') && body.has_method('attacked'):
body.attacked()


func die():
self.set_process(false)
_animator.current_animation = 'die'

从代码中可以看出来,怪物蛇在游戏中是不会检测与地面之间发生的碰撞的,所以在游戏关卡中,添加蛇场景的时候,必须把它的位置手动放置在地面上,否则会出现“蛇在空中移动”的现象。 joy

接着是光头敌人 NakedEnemy 场景的实现代码:

NakedEnemy.gd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
extends KinematicBody2D

# 省略部分代码……

export(float) var patrolRange = 3 * UNIT # 巡逻范围

var _walkSpeed: float = 3 * UNIT / 2.0 # 移动速度
var _gravity: float = 100 # 重力加速度
var _isStandingStill: bool = false # 站立不动

var _startPosition: float = 0.0 # 巡逻开始位置
var _moveDirection: int = 1 # 移动方向
var velocity: Vector2 = Vector2() # 速度
var _target = null # 攻击目标:玩家


func _physics_process(delta):
# 站着不动或者攻击对象不为空时,站立
if _isStandingStill || _target != null:
return

velocity.y += _gravity
velocity.x = _walkSpeed * _moveDirection
velocity = self.move_and_slide(velocity, FLOOR_NORMAL)

# 循环判断碰撞体是否有玩家(**这里不能检测到玩家在背后的情形**)
for index in range(self.get_slide_count()):
var collision = self.get_slide_collision(index)
if collision.collider.is_in_group('player'):
_target = collision.collider
_attack()
return

_animator.current_animation = _animations.walk

if self.is_on_wall():
_moveDirection = - _moveDirection
_sprite.flip_h = _moveDirection != 1
elif self.global_position.x >= _startPosition + patrolRange:
_standStill(-1)
elif self.global_position.x <= _startPosition - patrolRange:
_standStill(1)


# 攻击
func _attack():
if 'isDead' in _target && _target.isDead:
_target == null
return

if _target.has_method('attacked'):
_target.attacked()
_animator.current_animation = _animations.attack
yield(_animator, 'animation_finished')
_target = null


# 站立,nextDirection 表示站立时间过后转身
func _standStill(nextDirection: int):
_isStandingStill = true
_moveDirection = nextDirection
_animator.current_animation = _animations.idle
_timer.start()


# 站立时间超时
func _on_StandStillTimer_timeout():
_isStandingStill = false
_sprite.flip_h = _moveDirection != 1

# 省略部分代码……

这里根节点使用 KinematicBody2D 的好处是可以调用 move_and_slide 方法移动敌人,在代码中我给敌人添加了重力加速度,所以放置敌人的时候可以放在空中不需要贴着地面,但是这里的重力加速度值是手动设置的,并没有按照上一篇文章中讲的那样进行计算,也是为了方便简洁,够用就可以。

军人场景 SoldierEnemy 的代码与 NakedEnemy 的代码非常相似,这里我就不贴出来了,有兴趣的朋友可以到我的 Github 仓库下载源码查看。

游戏中其他场景包括:攻击效果子场景、敌人子弹、梯子、宝箱(恢复生命值)、木盒子(门)等这里不作一一介绍,结构和代码都很简单,在本系列之前的文章中都陆陆续续讨论过。

问题总结

总结一下这个游戏的开发过程。其实做任何一个游戏,不论游戏开发有多复杂,它的结构设计都是很重要的。比如设计这么一个简单的平台游戏,你需要考虑玩家所应有的一些功能、敌人的行为和种类、关卡地图的设计、游戏中互动元素的添加、甚至一些简单的剧情等等。设计完成接下来就是考虑如何使用游戏引擎来完成制作了。

本次小游戏的结构设计可以参考下图:

godot_star_hunter_scenes.png

这张图片场景名称并不相同,它是另一个游戏的场景结构,图片来源于 Thingsmatic 的一篇博客文章: Making a 2D platform game with Godot 3.0 ,尽管场景名字不同,但是总体结构是相似的,大家可以参考参考。

再来说说本次 Demo 中的一个非常重要的问题或者 BUG :玩家在敌人( KinematicBody2D )身后挨着走也不会受到攻击!也就是说,玩家可以从背后推着敌人走而敌人并不能检测到与玩家之间的碰撞,从而玩家可以安然无恙地“躲过一劫”!

kinematicbody2d_collision_test.gif

如何解决这个问题呢?至少有三种最直接的方式:

  1. 添加 Area2D 子节点用于检测碰撞
  2. 使用 Raycast 射线节点检测碰撞
  3. 使用 Physics2DDirectSpaceState.intersect_ray() 方法检测碰撞

为了让游戏更加简单易懂,我并没有在游戏中解决这个 BUG ,不过我会在后续文章中讨论这个问题,以及游戏中常用的 FSM 有限状态机制我想也会谈到,有兴趣的朋友可以自己先探索。 smiley

三、总结

小游戏算是完成了,但是还有太多不足之处,大家可以充分发挥自己的聪明才智,把你心目中的游戏打造的更加好玩、更加有特色,这里我能想到的一些可以在 Godot 中为游戏增光添彩的功能有:

  • 添加单边碰撞体作为地面
  • 玩家移动时的灰尘特效
  • 给玩家添加无敌状态
  • 添加空中移动平台
  • 给敌人添加 AI
  • 更多交互元素、更好的关卡设计等

本篇的 Demo 以及相关代码已经上传到 Github ,地址: https://github.com/spkingr/Godot-Demos , 这应该是春节前的最后一篇文章,原创不易,希望大家喜欢,我们 2019 新年见! smile

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


Comments: