欢迎阅读Godot3平台跳跃游戏实践系列文章,本系列将从创建工程开始,记录一个平台跳跃小游戏的制作过程,文章中如有错误或不妥之处欢迎指出。
上一篇文章中,我们编写了角色控制脚本,让角色根据玩家的输入而运动、显示相应动画与播放音效等,本篇文章将围绕如何调教摄像姬展开,完成后效果:
本篇涉及以下内容:
- Camera2D(摄像机2D)的使用
- ParallaxBackground(视差背景)的使用
- 摄像机范围限制
- 摄像机跟随与平滑移动
- 瓦片地图资源重用、碰撞绘制
- 单向碰撞的平台
创建摄像机
上一篇里我们已经基本做好了一个活蹦乱跳的角色,但是他可能跳着跳着就跳出屏幕外了,留下一个空荡荡的场景和发愣的玩家。这是因为游戏的摄影师不仅没有盒饭加鸡腿,甚至连盒饭都没有,罢工不干了。
没有摄影师,不会自己扮哦。在Player场景下,创建一个Camera2D节点,在Inspector面板中,将Current
属性勾选上:
工作区中可以看到一个紫色的框,表示摄像机的范围:
一个会自动跟随角色的摄像机就创建好了,接下来要看看效果怎样。首先在Level1场景中,将地图再多画一点,让角色可以多走一段距离。关于地图的绘制在(一)-搭建游戏世界
中已有介绍,本文在最后会做一些补充说明。
运行看看效果:
使用视差背景
可以发现一些问题,由于我们之前只是简单添加了一个Sprite作为背景,角色往前走之后就没有背景了;并且背景也没有视差,在角色前进时与地图一同后退,看起来十分生硬。
Godot中提供的ParallaxBackground
节点可以很轻松地解决这个问题。在Level1场景中,添加一个ParallaxBackground
节点,在其下添加一个ParallaxLayer
节点,然后把之前创建的背景Sprite节点拖至其下:
你可能注意到了我的Level1场景还有些其他变化,这是刚才画地图时做的调整,文章最后会介绍。
接着选中ParallaxLayer
节点,在Inspector面板中修改Scale
属性x修改为0.8,y保持1不变,这意味着背景在x方向上将以更慢的速度卷动;修改Mirroring
属性x为1024(背景图片的宽度),表示背景在x方向上会以1024像素为间隔不断重复。
运行看看效果:
可以看到背景以更缓慢的速度在后退,并且角色一直前进,背景会一直重复。
可以使用同样的方法加入一层云朵:
摄像机优化
现在的运行效果仍然存在问题,摄像机永远以角色为中心,导致显示了一些地图以外的区域,并且角色一运动,摄像机立即跟着运动,在跳动时体验不是很好。
加入范围限制
首先来给摄像机加上限制,避免显示地图外的部分。回到Player场景,选中Camera2D
节点,可以在Inspector面板中发现Limit
属性,分别有左、上、右、下四个值可以修改,可以试着修改这些值然后运行,看看摄像机的范围是否受到了限制。
但直接修改并不是最好的办法,我们根本不清楚这些值具体是多少,并且关卡是有可能改变的,当角色进入到另外一个关卡、或者小房间时,Limit
属性值就又不同了,我们需要一个一劳永逸的方法。
首先来搞清楚这些值需要设置成多少。观察Level1场景,能显示的范围便是当前地图的范围,那么上边与左边就很清楚了,都是0;而右边与下边将由最右下角的那个地图瓦片决定:
如何获得右下角地图瓦片的坐标呢?查看TileMap文档可知(Scene面板中右键TileMap节点,选择Open documentation
),可以使用get_used_rect
函数,获取到一个包含当前所有瓦片的Rect2
矩形,需要注意的是,不会包含空的瓦片区域:
Rect2
又是何方神圣?阅读文档可知它有起始点坐标、大小、终点坐标三个属性,其中终点坐标就是我们要的:
如果我们调用当前TileMap的get_used_rect
函数,将返回值打印出来的话,会发现该矩形区域差不多如下红框所示:
可以发现它不会包含空的区域。
那么就可以动手了,我们需要做的就是当角色进入到关卡时,获取当前关卡的范围数值,然后设置给Camera2D节点的Limit
属性。可以给Camera2D新增一个脚本,然后写这部分逻辑,这里我就偷懒直接写在Player脚本里了。在Player.gd中,新增一个set_camera_limits
函数:
func set_camera_limits():
# 获取父节点中包含的TileMap节点
var tile_map = $"../TileMap" as TileMap
if tile_map == null:
print_debug("没有找到关卡中的TileMap节点")
return
# 获取TileMap范围与瓦片大小
var rect = tile_map.get_used_rect()
var cell_size = tile_map.cell_size
# 给摄像机设置范围限制
var camera = $Camera2D
camera.limit_left = 0
camera.limit_top = 0
camera.limit_right = rect.end.x * cell_size.x
camera.limit_bottom = rect.end.y * cell_size.y
首先需要获取到关卡中的TileMap节点,"../TileMap"表示向上找到当前节点的父节点下的名为TileMap的节点,也就是Level1场景下的TileMap节点,写法较像文件的路径;
as TileMap
可以帮助检查获取到节点的类型,如果类型不对则会返回null;此外还能给编辑器一个暗示,让它知道"tile_map"变量是一个TileMap类型的变量,不要给我搞错了。
然后写一个看似没什么用但是有朝一日你可能会感谢你自己写了这一段代码的非空判断。
随后获取TileMap范围,同时获取一下瓦片的大小cell_size
,因为这里rect的包含的坐标是TileMap的本地坐标,单位是"个",所以需要乘以每个瓦片的大小来得出真实的游戏世界坐标。
最后便是拿到"Camera2D"节点,给它设置限制范围了。这里直接将原点设置为左、上限制,rect的终点设置为右、下限制。
set_camera_limits
函数需要在节点准备好时调用,在_ready
函数中调用即可:
func _ready():
set_camera_limits()
运行效果,摄像机不再显示地图以外的区域了:
直接获取父层级节点的方法虽然方便,但必须确保父层级中有一个名叫"TileMap"的TileMap类型节点,这不太安全且扩展性低,可读性也不好。实际项目中不建议这么写,可以考虑将一部分逻辑抽离至公共的关卡脚本基类中。
如果整个关卡区域不从原点开始,那么Rect2
的起始点属性position
就能派上用场了,这取决于地图是怎么设计的。
目前为止Player.gd的完整代码:
extends KinematicBody2D
const FLOOR_NORMAL = Vector2.UP
var gravity = 3800
var jump_force = -1200
var max_speed = 400
var velocity = Vector2(0, 0)
onready var AnimatedSprite = $AnimatedSprite
onready var AudioStreamPlayer = $AudioStreamPlayer
func _ready():
set_camera_limits()
func set_camera_limits():
"""设置摄像机范围限制"""
# 获取父节点中包含的TileMap节点
var tile_map = $"../TileMap" as TileMap
if tile_map == null:
print_debug("没有找到关卡中的TileMap节点")
return
# 获取TileMap范围与瓦片大小
var rect = tile_map.get_used_rect()
var cell_size = tile_map.cell_size
# 给摄像机设置范围限制
var camera = $Camera2D
camera.limit_left = 0
camera.limit_top = 0
camera.limit_right = rect.end.x * cell_size.x
camera.limit_bottom = rect.end.y * cell_size.y
func _physics_process(delta):
# 水平方向运动
# 获取水平方向输入
var direction = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
# 使用函数计算速度
velocity.x = lerp(velocity.x, direction * max_speed, 0.2)
# 动画
if direction != 0:
AnimatedSprite.flip_h = direction < 0 # 方向为负则翻转
AnimatedSprite.play("walk") # 播放行走动画
else:
AnimatedSprite.play("idle") # 播放空闲动画
# 竖直方向运动
if is_on_floor():
# 位于地面,获取跳跃输入
if Input.is_action_just_pressed("ui_up"):
velocity.y = jump_force
# 随机范围音高
AudioStreamPlayer.pitch_scale = rand_range(0.7, 1)
AudioStreamPlayer.play() # 播放跳跃音效
else:
AnimatedSprite.play("jump") # 播放跳跃动画
# 施加重力
velocity.y += gravity * delta
velocity = move_and_slide(velocity, FLOOR_NORMAL)
跳动时的跟随
角色跳动时摄像机几乎是立即跟随角色,令人感觉整个画面很抖。这里的设置比较好调整,选中Camera2D节点,Inspector面板中勾选Editor
下的Draw Drag Margin
,工作区中可以看到蓝色的框:
当角色移动到框边时,摄像机便会移动,调整框的大小即可控制摄像机的跟随。Inspector面板中,调整Drag Margin
属性为你喜欢的值即可,如果希望摄像机平滑移动,还可以勾选Smoothing
的Enabled
:
运行效果:
瓦片地图的一些补充
最后再补充一些绘制瓦片地图相关的内容。
资源的重用
前面有提到Level1场景有些变化,主要是我又创建了一个叫做"Platforms"的TileMap节点,专门用来画一些悬空的平台。原因是TileMap有一个局限性,它只能按网格绘制瓦片,而将一些平台与网格稍微错开显得更没那么死板(个人感觉),所以我创建了一个新的TileMap节点,并将它位移了一段距离。
那么创建一个新的TileMap,又要从头制作TileSet吗?我们可以直接右键TileMap节点选择Duplicate
来复制一份,但里面画好的地图也被会被复制。所以这里不如直接保存制作好的TileSet,之后复用这个TileSet就好了。
选中TileMap节点,Inspector面板中点击TileSet
属性右侧,可以看到有Load
、Save
等选项,这里选择Save
将其保存为"TileSet.tres"文件:
那么新建的TileMap节点里,只需要Load
这个资源文件就行了。
如果你复制了某个碰撞体,然后调整它的大小,发现被复制者的大小也同时在改变,这其实是复制的碰撞体的形状指向的是被复制者的那一份,如果希望二者不再有关联,那么只需选择复制者的Make Unique
即可:
单向碰撞
角色在这里头都碰肿了:
按习惯来说,这种视觉上靠后的平台是可以从下面跳上去的,但由于碰撞体才不管什么上下,统统给挡住了。这里只需要选中这个碰撞形状,在Inspector面板中勾选One Way Collision
属性即可:
如果希望达到相反效果呢?只需要调整Transform
下的Rotation Degrees
属性,让碰撞形状转个180度即可。
我知道怎么上去了,现在我想直接下来怎么办?别担心,你还可以从旁边下来 这里先挖个坑,后续的文章再来填。
TileSet中绘制碰撞形状
其实地图的碰撞形状可以不用一个个画,在TileSet中指定每个瓦片的碰撞形状,绘制瓦片时碰撞形状也会自动绘制上去。不过个人感觉这种方式不太灵活,所以文章中依然是手动绘制碰撞。
选中TileMap
并打开TileSet
属性,点击任一瓦片激活编辑界面:
点击Collision
按钮,下方会出现一排工具,包含矩形以及多边形,选择形状并在瓦片上绘制即可。
如果不想让碰撞形状充满整个瓦片,可以在Inspector面板中对Subtile Size
属性进行调整。
单向碰撞同样可以通过Selected Collision One Way
属性来调整。
至此摄像姬已经基本调教完成,游戏世界也开始有模有样了,下一篇文章将围绕加入敌方角色展开。
下一篇:[Godot3游戏引擎实践]平台跳跃小游戏(六)-创建敌方角色