推箱子游戏的基本代码

简介

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

整体项目内容

游戏的素材可以在这里下载:https://kenney.nl/assets/sokoban

游戏运行后的结果是这样: 运行效果

场景

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

场景节点

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

场景布置

箱子

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

箱子节点

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

箱子UI效果

箱子完成后,选择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的官方解释是“查询与射线相交的最近物体”,意思也就是用这个射线来探测移动方位前方的物体。

玩家节点

角色UI效果

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

raycast2d配置

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

角色四方位动画

在最顶层的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作为顶级的节点,再添加一个图像和一个碰撞的节点,这里需要注意一点,碰撞的节点不用覆盖这个图像,只需比中心点大一些就可以了。应该目的地的元素还需要设置一个判断箱子元素进入和退出的信号,如果碰撞的区域太大,会导致判断失误。

目的地节点

终点UI效果

和箱子一样,目的地也需要一个分组。这个分组命名为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实现的笔记,敬请期待。