目录
我们使用godot来开发一款家喻户晓的贪吃蛇小游戏,结合剪刀石头布规则:玩家和食物都有三种属性:剪刀、石头和布;
游戏规则:当玩家是剪刀的时候,只能吃掉剪刀和布,石头会来追捕玩家。吃掉剪刀和布会变长,吃掉布还会变成布,这时可以反过来吃掉石头。被克制属性追到或者撞到墙上会被击杀。灵感是洗澡(*^▽^*)催发的~。
我们先完成贪吃蛇的基础玩法:蛇移动,吃掉食物变长。
下面是gif游戏图:
1、新建项目
在左上角的“项目——项目设置——窗口——拉伸”,我们设置模式为2d,比例为keep,这样子全屏也不会让游戏被拉伸了
2、设计游戏画面
如图所示,新建Control根节点节点,重命名为"UI";在UI节点下新建ColorRect,重命名为BG,设置颜色偏黑色;在UI节点下新建CanvasLayer节点用来显示分数,CanvasLayer节点下新建Label节点,重命名为Score(CanvasLayer节点默认z值为1,也就是在图片等节点之上,避免图片覆盖了分数)。
然后点击Score节点,Control——Theme Overrides——Fonts:新建DynamicFont,点击Font将我们的字体资源文件 Comfortaa-Bold.ttf 拖进去,并在Settings中改变字体大小等。
这是初始游戏画面:
3、添加玩家和移动脚本
如图所示,新建场景,新建“KinematicBody2D”根节点,重命名为Player,新建两个子节点:ColorRect和CollisionShape2D,ColorRect的颜色赋上自己喜欢的并且居中;CollisionShape2D新建Shape,把ColorRect覆盖住。
然后Player新建脚本Player.gd:
extends KinematicBody2D
#移动方向和移动速度 ,export让速度可以随时更改
var direction = Vector2(0,0)
export var speed = 100
func _ready():
pass # Replace with function body.
# 用物理刷新来移动,避免delta值不同使得位移不同。
func _physics_process(delta):
if Input.is_action_pressed("ui_up"):
direction = Vector2.UP
if Input.is_action_pressed("ui_down"):
direction = Vector2.DOWN
if Input.is_action_pressed("ui_left"):
direction = Vector2.LEFT
if Input.is_action_pressed("ui_right"):
direction = Vector2.RIGHT
position += direction * speed * delta
通过注释不难理解。现在我们把玩家放在UI场景中:
4、添加食物场景和食物脚本
类似构建Player场景,新建Food场景:
并且为Food添加脚本,Food.gd:
extends KinematicBody2D
#吃掉食物的价值
var value
# 死亡发出信号,修改分数等操作
signal die
5、吃与被吃的实现
首先让我们在左上角点击项目——项目设置——层名称——2D物理,新建三个层次:World、Player和Food
Player场景中,根节点的Collision的Layer我们设置为Player,Mask设置为Food(Layer可以理解为所在的层次,Mask可以理解为要与哪个层次发生碰撞)。Food场景同理。
被吃的逻辑
然后在Player场景中,添加“Area2D”子节点,重命名为EatArea2D,并且添加“CollisionShape2D”节点然后新建Shape,把ColorRect覆盖住。Food场景也是这个操作,“Area2D”重命名为HurtArea2D。然后点击HurtArea2D,在右边的“节点”——“信号”我们双击“area_entered(area:Area2D)”信号,连接到Food所在的脚本。
这个信号是当玩家触碰到食物的时候会调用的,当触碰到食物的时候,我们传递死亡信号表示玩家吃到了食物,并且销毁食物场景。
吃到的逻辑
我们修改Player的代码:
extends KinematicBody2D
#移动方向和移动速度 ,export让速度可以随时更改
var direction = Vector2(0,0)
export var speed = 100
var player_score
onready var Food = preload("res://Food.tscn")
onready var score = $"../CanvasLayer/Score"
onready var color_rect = $ColorRect
func _ready():
randomize() #randomize()是用来修改随机种子数
create_food()
player_score = 0
# 用物理刷新来移动,避免delta值不同使得位移不同。
func _physics_process(delta):
if Input.is_action_pressed("ui_up"):
direction = Vector2.UP
if Input.is_action_pressed("ui_down"):
direction = Vector2.DOWN
if Input.is_action_pressed("ui_left"):
direction = Vector2.LEFT
if Input.is_action_pressed("ui_right"):
direction = Vector2.RIGHT
position += direction * speed * delta
func create_food():
var food_tscn = Food.instance()
# call_deferred是在空闲时间调用方法,避免由于节点忙碌调用失败
get_parent().call_deferred("add_child", food_tscn)
set_food_position(food_tscn)
food_tscn.value = 1
food_tscn.connect("die",self,"change_score",[food_tscn.value])
func set_food_position(food_tscn):
var random_x = 0
var random_y = 0
var food_size = food_tscn.get_node("ColorRect").rect_size
var player_size = color_rect.rect_size
var win_size = Vector2(get_viewport().size.x,get_viewport().size.y)
var overlap = true
# 循环,直到找到不与蛇重叠的食物位置
while overlap:
random_x = randi() % (int(win_size.x - food_size.x)) + food_size.x/2
random_y = randi() % (int(win_size.y - food_size.y)) + food_size.y/2
var food_rect = Rect2(random_x - food_size.x, random_y - food_size.y, food_size.x, food_size.y) # 食物的碰撞区域
#
# # 检查食物是否与玩家的身体重叠
overlap = false
var player_rect = Rect2(global_position - Vector2(player_size.x/2, player_size.y/2), Vector2(player_size.x, player_size.y))
if food_rect.intersects(player_rect):
overlap = true
food_tscn.position = Vector2(random_x, random_y)
func change_score(value):
# 增加分数并且生成新的食物。
# create_food()这个方法随时都可以调用,这意味着我们可以用计时器调用,也可以为玩家创建技能生成食物来调用
player_score += value
score.text = "SCORE: " + str(player_score)
create_food()
可以看到,在create_food()方法中,我们新建food之后,进行信号连接,die这个信号发送之后会调用change_score(value)方法,这个时候我们可以来回增加分数并且生成新的食物了:
6、变长变长再变长
传统的贪吃蛇变长需要使用列表存储,反复移动列表中的元素使得移动的时候尾巴转弯是正确的,我们在这次开发中使用Line2D来模拟尾巴,实现拖尾效果。
优点是简单快速并且能自己改变形状。
缺点是长度会受到移动速度的影响。
新建Line2D的场景,将其拖到UI场景中,Line2D新建曲线,设置如下:
在Player.gd中,我们添加了create_tail()函数在_physics_process(delta)中调用,长度是在change_score(value)中变长的。
extends KinematicBody2D
#移动方向和移动速度 ,export让速度可以随时更改
var direction = Vector2(0,0)
export var speed = 100
export var player_length = 0
var player_score
var player_size
onready var Food = preload("res://Food.tscn")
onready var score = $"../CanvasLayer/Score"
onready var color_rect = $ColorRect
onready var line_2d = $"../Line2D"
func _ready():
randomize() #randomize()是用来修改随机种子数
player_score = 0
player_size = color_rect.rect_size
create_food()
# 用物理刷新来移动,避免delta值不同使得位移不同。
func _physics_process(delta):
if Input.is_action_pressed("ui_up"):
direction = Vector2.UP
if Input.is_action_pressed("ui_down"):
direction = Vector2.DOWN
if Input.is_action_pressed("ui_left"):
direction = Vector2.LEFT
if Input.is_action_pressed("ui_right"):
direction = Vector2.RIGHT
position += direction * speed * delta
# 制作尾巴
create_tail()
func create_tail():
# 在line2D中,是没有长度这个概念的,只有宽度,而我们在生成点的同时把最早生成的点删掉,就有长度的产生了
line_2d.width = color_rect.rect_size.x
line_2d.add_point(self.position)
if(line_2d.get_point_count() >= player_length):
line_2d.remove_point(0)
func create_food():
var food_tscn = Food.instance()
# call_deferred是在空闲时间调用方法,避免由于节点忙碌调用失败
get_parent().call_deferred("add_child", food_tscn)
set_food_position(food_tscn)
food_tscn.value = 1
food_tscn.connect("die",self,"change_score",[food_tscn.value])
func set_food_position(food_tscn):
var random_x = 0
var random_y = 0
var food_size = food_tscn.get_node("ColorRect").rect_size
var win_size = Vector2(get_viewport().size.x,get_viewport().size.y)
var overlap = true
# 循环,直到找到不与蛇重叠的食物位置
while overlap:
random_x = randi() % (int(win_size.x - food_size.x)) + food_size.x/2
random_y = randi() % (int(win_size.y - food_size.y)) + food_size.y/2
var food_rect = Rect2(random_x - food_size.x, random_y - food_size.y, food_size.x, food_size.y) # 食物的碰撞区域
#
# # 检查食物是否与玩家的身体重叠
overlap = false
var player_rect = Rect2(global_position - Vector2(player_size.x/2, player_size.y/2), Vector2(player_size.x, player_size.y))
if food_rect.intersects(player_rect):
overlap = true
food_tscn.position = Vector2(random_x, random_y)
func change_score(value):
# 增加分数并且生成新的食物。
# create_food()这个方法随时都可以调用,这意味着我们可以用计时器调用,也可以为玩家创建技能生成食物来调用
player_score += value
score.text = "SCORE: " + str(player_score)
player_length += 1
create_food()
呼呼,我们的最终结果是下面的gif:
之后我们的剪刀石头布会用到状态机,下次见~
补充,这是后续的最终版实现教程: