推箱子游戏的基本代码
简介
推箱子的游戏,在godot中开发起来,其实还是比较简单的,就是角色组件加上单方向的碰撞检测。 主要是讲解一下写推箱子游戏的一些思路和代码,也因为是主要讲一下代码,所以UI换面的布置就不详细讲述。 本教程的演示版本为:godot v3.5.2.stable.official 不同的版本可能有细微的差异。 权当是学习godot的一个实例练习。整个项目的文件树结构如下图,因为就是一个练习的作品,没有去制作多个目录存放不同功能的源文件系统。

游戏的素材可以在这里下载:https://kenney.nl/assets/sokoban
游戏运行后的结果是这样:

场景
游戏的场景由一个tilemap组成的背景,一个玩家节点,和数个箱子和箱子的放置终点组成。

在来看看场景的图片,玩家人物、箱子和背景中的墙,都有碰撞区域。这是一个简单的演示,就只是摆放了一个长方形和两个箱子而已。

箱子
箱子做成一个独立的场景,然后拖到主场景中就好,场景的顶部节点是一个KinematicBody2D,命名为box,下面的子节点有一个箱子的图片,一个碰撞的图形,还有一个检查物体的射线。射线的位置定在箱子的中心点上。碰撞的形状覆盖整个箱子。

箱子场景的最终效果是这样的

箱子完成后,选择box这个节点,在节点标签中,选择【分组】,在【管理分组】下面输入一个分组的名称,然后点击添加,这里我们给箱子分配到一个叫做“box”的组里面,后面的其他节点的代码会用是否在“box”组里面来判断,碰撞到的是不是箱子。所以这一步挺重要,后面的目的地也是用同样的办法来添加到一个叫

在箱子节点的最顶层节点中添加脚本文件,代码如下:
var directions = {
"ui_up": Vector2.UP,
"ui_down": Vector2.DOWN,
"ui_left": Vector2.LEFT,
"ui_right": Vector2.RIGHT,
}
onready var ray = $RayCast2D
func move(dir):
var pos = directions[dir] * 64
ray.cast_to = pos
ray.force_raycast_update()
if ray.is_colliding():
return false
else:
self.position += pos
return true
代码说明: 代码中先定义一个有四个方位的字典类型的变量,然后初始化射线。最后是这个场景的核心代码,移动物体并返回一个布尔值给回到玩家角色,然后玩家角色再根据反馈决定下一步需要怎么做。这里面。pos除了本来的移动再成语64,是因为这个项目的每一个元素,都是定义为64乘64像素,乘以64像素就是为了让箱子和角色一样,每次指移动一格。
玩家角色
玩家角色独立做成一个场景,命名为player,角色的图像和碰撞节点都左上角对齐,方便后续的布置下载的素材中,角色四个方向的动画,所以添加了一个AnimationPlay的节点,做了四个方向的动画。再添加一个RayCast2D,RayCast2D的官方解释是“查询与射线相交的最近物体”,意思也就是用这个射线来探测移动方位前方的物体。


RayCast2D的【Cast To】里面的参数,建议y轴修改为和素材中的长度一致。然后修改其坐标在图标的中心。

角色的四个方位动画,按照系统按键的默认名称命名,方便后面的代码控制方位。

在最顶层的player节点中添加脚本文件并写入下面的代码:
extends KinematicBody2D
var directions = {
"ui_up": Vector2.UP,
"ui_down": Vector2.DOWN,
"ui_left": Vector2.LEFT,
"ui_right": Vector2.RIGHT,
}
onready var ray = $RayCast2D
func _ready():
$AnimationPlayer.play("ui_down")
func _input(event):
for dir in directions:
if event.is_action_pressed(dir):
move(dir)
func move(dir):
$AnimationPlayer.play(dir)
var pos = directions[dir] * 64
ray.cast_to = pos
ray.force_raycast_update()
if ray.is_colliding():
var collider = ray.get_collider()
if collider.is_in_group("box"):
if collider.move(dir) :
self.position += pos
else:
self.position += pos
代码说明:
定义一个字典写四个方位,这个纯粹是个人习惯,主要是因为我懒,后面直接用for循环,里面写一个if判断来判断输入,而没有写成if...elif...重复四次。
还需要在开始载入的时候,初始化RayCast2D,
_ready()函数,在节点开始的时候,播放的是角色向下的动画。
输入函数中,就用一个for循序的内容,再判断当前输入的是四个方位中的哪一个,并执行移动的动作,四个方位的动作基本要做的事情就只是方位的不同,其他的都一致,所以将它们定义在一个传入方位参数的名为move的函数中。
move()函数传入了方位的值,然后先确定播放角色对应方位的动画,和移动一格的距离,角色和场景定义了一格为64像素,所以就乘以64。射线也更正移动和更新方位。接下来是看射线是否有碰撞,并且获取到该物体的信息,如果有碰撞的且物体的组名为box,就让该物体向对应的方向移动一格且自己也移动一格,如果没有碰撞则角色自己移动一格。
终点
目的地的设置就比较简单,使用一个Area2D作为顶级的节点,再添加一个图像和一个碰撞的节点,这里需要注意一点,碰撞的节点不用覆盖这个图像,只需比中心点大一些就可以了。应该目的地的元素还需要设置一个判断箱子元素进入和退出的信号,如果碰撞的区域太大,会导致判断失误。


和箱子一样,目的地也需要一个分组。这个分组命名为dest,将用在后面注册机的公共代码中,用来判断是否所有的目的地都有箱子。

var isActive = false
func _on_goal_body_entered(body):
if body.is_in_group("box"):
isActive = true
func _on_goal_body_exited(body):
if body.is_in_group("box"):
isActive = false
代码说明: 整体代码分为三部分,第一部分是定义一个变量,用来判断这个目的地是否有箱子到达。第二部分是一个当有现在进入这个图形的时候执行上面的判断,首先判断这个图形是否是箱子的“box”组中的元素,因为玩家角色也有可能会因为要推箱子前进而踩在这个图形上面。所以要判断是否是箱子,如果是箱子就把这个的参数修改为是箱子。第三部分的函数和第二部分的函数相反,当箱子离开这个区域,就见参数修改为否,主要是因为,你可能设计两个目的地在一起,你推第一个箱子可以需要通过第一个点到达第二个点,这样离开了第一个点后,如果不修改离开后参数为否,就会产生一个bug,一个箱子经过两个目的地,游戏就完成了。
返回主场景添加公共的代码
上面的设置都完成后,就会到主场景,在场景中添加一个脚本文件,写下面的代码:
extends Node2D
var game_finish = false
func _process(delta):
check_finish()
func check_finish():
if !game_finish:
var destinations = get_tree().get_nodes_in_group("dest")
var finish_num = 0
for destination in destinations:
if destination.isActive == true:
finish_num += 1
if finish_num == destinations.size():
game_finish = true
print("game finish")
代码说明: 整体的代码就是为了完成一个目的,就是检测有多少个目的地,目的地是否已经有箱子在上面,如果这两个条件都完成,就游戏已经完成,打印出游戏完成的字样到终端。
结尾
完成以上的步骤,则一个简易的推箱子游戏的组装就完成了,接下来就可以进行游玩测试了。
本项目的完整代码,可以通过下面的百度网盘下载。 链接: https://pan.baidu.com/s/12u5MLEC40Ik4Bgbz-O9Fqw?pwd=tx6s 提取码: tx6s
这个笔记是用godot3实现的,有机会在写一个使用godot4实现的笔记,敬请期待。