目录
前提,先看完这篇文章:当贪吃蛇碰上剪刀石头布——godot版(用line2D)-CSDN博客
这次,我们将游戏制作完毕,这是最终的游戏gif图(和第一部分教学的gif不一样,进行了小优化):
1、代码书写前的项目建设
首先将图片粘贴到项目中(不要像我这么乱QAQ,可以写几个文件夹scenes、scripts来放置资源):https://pan.baidu.com/s/1wpbNaofHT5ENBSZDuCEWMA?pwd=1234
之后为Player.tscn添加“Sprite”,为Food.tscn添加“Sprite”和新的“Area2D”节点,重命名为ChaseArea2D,Area2D依旧添加“CollisionShape2D”。Sprite用来存放图片,表示属性;ChaseArea2D用来探测玩家的位置。结构分别如图所示:
设置ChaseArea2D的Collision中,取消Layer的层次,Mask层次为Player:
最后我们把Food场景进行分组,这样子可以统一销毁:
2、添加三种属性:剪刀石头布
我们修改Player.gd代码:
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"
onready var player_sprite = $Sprite
onready var RockTex = preload("res://Rock.png")
onready var ScissorsTex = preload("res://Scissors.png")
onready var PaperTex = preload("res://Paper.png")
var TYPE = ["ROCK","SCISSORS","PAPER"]
var eat_relation = {"SCISSORS":"PAPER","ROCK":"SCISSORS","PAPER":"ROCK"}
var player_type
signal player_type_change
func _ready():
randomize() #randomize()是用来修改随机种子数
player_score = 0
player_size = color_rect.rect_size
player_type = TYPE[randi() % TYPE.size()] # 随机赋上一个属性
set_sprite(player_sprite,player_type)
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.food_type = TYPE[randi() % TYPE.size()]
set_sprite(food_tscn.find_node("Sprite"),food_tscn.food_type)
food_tscn.connect("die",self,"change_score",[food_tscn])
self.connect("player_type_change",food_tscn,"_on_player_type_change",[self])
func set_sprite(sprite,type):
if type == "ROCK":
sprite.set_texture(RockTex)
elif type == "SCISSORS":
sprite.set_texture(ScissorsTex)
elif type == "PAPER":
sprite.set_texture(PaperTex)
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(food_tscn):
# 增加分数并且生成新的食物。
# create_food()这个方法随时都可以调用,这意味着我们可以用计时器调用,也可以为玩家创建技能生成食物来调用
player_score += food_tscn.value
score.text = "SCORE: " + str(player_score)
create_food()
func _on_EatArea2D_area_entered(area):
if area.name == "HurtArea2D":
var food = area.get_parent()
var food_type = food.food_type
if player_type == food_type:# 吃到相同属性变长
pass
elif player_type == eat_relation[food_type]:#如果玩家属性是食物克制的属性,死亡
pass
elif food_type == eat_relation[player_type]:#如果食物属性是玩家克制的属性,变成此属性然后变长
pass
其中,我们先获取图片节点,并且获取图片资源,并用字典eat_relation来表示吃与被吃的关系,player_type用来表示玩家当前属性,从TYPE列表中获取:
onready var player_sprite = $Sprite
onready var RockTex = preload("res://Rock.png")
onready var ScissorsTex = preload("res://Scissors.png")
onready var PaperTex = preload("res://Paper.png")
var TYPE = ["ROCK","SCISSORS","PAPER"]
var eat_relation = {"SCISSORS":"PAPER","ROCK":"SCISSORS","PAPER":"ROCK"}
var player_type
set_sprite(sprite,type)是通用的方法,只需要传递图片节点和当前的type属性,就可以修改图片了:
func set_sprite(sprite,type):
if type == "ROCK":
sprite.set_texture(RockTex)
elif type == "SCISSORS":
sprite.set_texture(ScissorsTex)
elif type == "PAPER":
sprite.set_texture(PaperTex)
在创建食物create_food()中,我们自定义了一个信号player_type_change与食物连接,当玩家属性修改的时候,通知每一个Food属性修改了。
在玩家触碰的食物的方法_on_EatArea2D_area_entered中,我们先判断食物area的名称,避免和ChaseArea2D弄混,其中三个if用来为后面的逻辑实现做准备。
func _on_EatArea2D_area_entered(area):
if area.name == "HurtArea2D":
var food = area.get_parent()
var food_type = food.food_type
if player_type == food_type:# 吃到相同属性变长
pass
elif player_type == eat_relation[food_type]:#如果玩家属性是食物克制的属性,死亡
pass
elif food_type == eat_relation[player_type]:#如果食物属性是玩家克制的属性,变成此属性然后变长
pass
接下来是Food.gd的代码(注意,最后的两个方法是由ChaseArea2D节点的信号通过连接生成的,直接复制粘贴是不行的):
extends KinematicBody2D
#吃掉食物的价值
var value
var food_type
var eat_relation = {"SCISSORS":"PAPER","ROCK":"SCISSORS","PAPER":"ROCK"}
# 死亡发出信号,修改分数等操作
signal die
enum{IDLE,CHASE}
var catch_type = IDLE
var chaseArea2D_player = null # chaseArea2D探测区域中是否有player
func _physics_process(delta):
if catch_type == IDLE:
pass
elif catch_type == CHASE:
pass
# 信号传递,玩家状态变化时候,通知传递给每个食物
func _on_player_type_change(player):
if player.player_type == eat_relation[food_type] and chaseArea2D_player != null:
catch_type = CHASE
else:
catch_type = IDLE
func _on_HurtArea2D_area_entered(area):
if area.name == "EatArea2D":
emit_signal("die")
queue_free()
func _on_ChaseArea2D_area_entered(area):
chaseArea2D_player = area.get_parent()
var player_type = chaseArea2D_player.player_type
if player_type == eat_relation[food_type]:# 变成追杀模式
catch_type = CHASE
func _on_ChaseArea2D_area_exited(area):
chaseArea2D_player = null
catch_type = IDLE
其中food_type和eat_relation和Player.gd中的一致,然后弄了新的枚举类,catch_type用来存储食物的状态:静止或者追捕。
_physics_process中的两个if时刻对食物状态进行监控,并迅速执行当前状态的代码。
ChaseArea2D传递了两个方法,分别是玩家进入探测区,判断是否要变成追捕状态和玩家退出探测区,变成静止状态。
3、玩家和三种属性之间的逻辑实现
主要是在Player.gd中的_on_EatArea2D_area_entered(area)进行实现
玩家相同属性
很简单的代码,直接变长:
if player_type == food_type:# 吃到相同属性变长
player_length += 1
玩家被克属性
若是玩家被克制的属性,主要是对食物代码的修改。
- 一是要对探测区进行处理:进入食物探测区,食物加速到最大速度然后进行追捕。
- 二是在追捕过程,若玩家修改了属性,则摩擦减速并且静止;若触碰到玩家,则玩家死亡,游戏结束。
我们先为食物Food添加速度、摩擦力、最大速度和加速度:
var velocity = Vector2(0,0)
export var FRICTION = 2000
export var MAX_SPEED = 200
export var ACCELERATION = 2000
然后在_physics_process(delta)中,我们修改了代码,下面有代码解释:
func _physics_process(delta):
if catch_type == IDLE:# 变成静止,摩擦减速
velocity = velocity.move_toward(Vector2(0,0),FRICTION * delta)
elif catch_type == CHASE:
if chaseArea2D_player == null:
catch_type = IDLE
else:
# 获取指向玩家的向量
var direction = self.global_position.direction_to(chaseArea2D_player.global_position)
velocity = velocity.move_toward(direction * MAX_SPEED,ACCELERATION * delta)
velocity = move_and_slide(velocity)
- 1、velocity = move_and_slide(velocity)是用来移动的,作用是沿着velocity向量移动物体,比如velocity为(1,1)时,按(1,1)像素每秒速度移动。当我们逐步提高velocity的时候,将会有加速的效果;当我们逐步减少velocity至(0,0)的时候,将会有摩擦的效果。
- 2、当食物为静止状态时,我们调用velocity.move_toward(Vector2(0,0),FRICTION * delta),参数表示velocity向量向(0,0)以FRICTION * delta这个时间减少。
- 3、当食物追捕状态时,我们首先获取食物指向玩家的向量,然后velocity提高接近这个向量。
完成食物Food的代码后,让我们来完善玩家Player的代码吧。
首先在UI场景中,新建Timer节点,重命名为"Create_Food_Timer",然后连接timeout信号到Player的代码中,每隔一段时间生成食物;然后在CanvasLayer新建Button节点,重命名为RestartButton,并修改Font字体,然后连接pressed信号到Player的代码中。在Player.gd中,我们新建了over_game()和restart_game()这两个方法:
onready var create_food_timer = $"../Create_Food_Timer"
onready var restart_button = $"../CanvasLayer/RestartButton"
func over_game():
create_food_timer.stop()# 停止计时器
restart_button.text = "ReStart"
restart_button.show()
line_2d.clear_points()
set_physics_process(false)
position = Vector2(get_viewport().size.x/2,get_viewport().size.y/2)
get_tree().call_group("Food","queue_free")
func restart_game():
create_food_timer.start()
set_physics_process(true)
randomize() #randomize()是用来修改随机种子数
player_score = 0
score.text = "SCORE: " + str(player_score)
player_length = 4
player_size = color_rect.rect_size
player_type = TYPE[randi() % TYPE.size()] # 随机赋上一个属性
direction = Vector2(0,0)
set_sprite(player_sprite,player_type)
create_food()
over_game()是当玩家触碰到被克属性时候执行的:
elif player_type == eat_relation[food_type]:#如果玩家属性是食物克制的属性,死亡
over_game()
在over_game()中,做了一系列暂停和销毁,包括暂停计时器和物理刷新(禁止玩家移动),销毁line2D线条和场景中的食物。
restart_game()是当玩家点击按钮时候调用的,做了一系列初始化,包括玩家分数、种子数、属性、移动向量等的初始化,并启动了计时器。
同时我们也对Player.gd的_ready()进行了修改:
func _ready():
restart_button.text = "Start"
restart_button.show()
set_physics_process(false)
玩家克制属性
当完成了被克属性的代码后,基本上结束了文章最难的部分啦(*^▽^*)。
玩家克制属性,则触碰之后变长并且修改属性,然后通知每一个食物玩家属性修改。
elif food_type == eat_relation[player_type]:#如果食物属性是玩家克制的属性,变成此属性然后变长
player_length += 1
player_type = food_type
set_sprite(player_sprite,food_type)
emit_signal("player_type_change")# 跟food说player的type改变,food是否要修改状态,变成静止或者追捕
4、墙壁区域的构建(修改玩家移动)
最后碰到墙壁就不要死亡吧,在墙壁停止就好。墙壁制作也很简单,让我们来制作墙壁:
首先在UI场景中新建“node”节点,重命名为Wall,然后在node节点中,新建“StaticBody2D”,重命名为“WallStaticBody2D”,将其Collision中的Layer层去掉,Mask层为Player和Food:
然后在StaticBody2D下创建子节点“CollisionShape2D”,并且复制四个“StaticBody2D”如图:
CollisionShape2D的形状在边界就可以了:
然后,我们优化了玩家移动的代码:
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
move_and_slide(direction * speed)
# 制作尾巴
create_tail()
使用了move_and_slide(direction * speed) 代替了直接改变position,因为move_and_slide可以让玩家这个“KinematicBody2D”运动体与“StaticBody2D”静态墙壁进行交互,运行is_on_wall()等代码。
5、一些问题细节的修改,让你的游戏看起来好看
食物重叠
食物在移动过程中,会重叠到一起,看不清有哪些食物,因此我们要避免食物重叠,只需要改变Food的碰撞的Mask就可以了,增加一个Food:
距离玩家太近
原因是我们在set_food_position()函数中,只是不让食物和玩家重叠而已,并没有让食物距离玩家多远才确定食物的生成位置,修改Player.gd代码如下:
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 isClose = true
# 循环,直到找到距离玩家远的位置,找到退出循环
while isClose:
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 distance = (global_position - Vector2(random_x,random_y)).length()
if distance >= distance_to_player:
isClose = false
else:
isClose = true
food_tscn.position = Vector2(random_x, random_y)
6、bug的总结
遇上一个问题,是这一行在create_food()函数中的代码:
self.connect("player_type_change",food_tscn,"_on_player_type_change",[self])
最初我传递的参数是player_type变量,而不是self,它并不会随着player_type值的变化而变化,因此当我们emit_signal传递信号,让食物决定是否追杀或者静止的时候会有问题,需要传递self玩家自身,使用self.player_type才能获取当前的玩家属性。
最终Player.gd和Food.gd的代码如下(注意带信号标签的函数,是不能直接复制粘贴的哦):
Player.gd
extends KinematicBody2D
#移动方向和移动速度 ,export让速度可以随时更改
var direction = Vector2(0,0)
export var speed = 200
export var player_length = 0
export var distance_to_player = 400
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"
onready var player_sprite = $Sprite
onready var RockTex = preload("res://Rock.png")
onready var ScissorsTex = preload("res://Scissors.png")
onready var PaperTex = preload("res://Paper.png")
onready var create_food_timer = $"../Create_Food_Timer"
onready var restart_button = $"../CanvasLayer/RestartButton"
var TYPE = ["ROCK","SCISSORS","PAPER"]
var eat_relation = {"SCISSORS":"PAPER","ROCK":"SCISSORS","PAPER":"ROCK"}
var player_type
signal player_type_change
func _ready():
restart_button.text = "Start"
restart_button.show()
set_physics_process(false)
# 用物理刷新来移动,避免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
move_and_slide(direction * speed)
# 制作尾巴
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.food_type = TYPE[randi() % TYPE.size()]
set_sprite(food_tscn.find_node("Sprite"),food_tscn.food_type)
food_tscn.connect("die",self,"change_score",[food_tscn])
self.connect("player_type_change",food_tscn,"_on_player_type_change",[self])
func set_sprite(sprite,type):
if type == "ROCK":
sprite.set_texture(RockTex)
elif type == "SCISSORS":
sprite.set_texture(ScissorsTex)
elif type == "PAPER":
sprite.set_texture(PaperTex)
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 isClose = true
# 循环,直到找到距离玩家远的位置,找到退出循环
while isClose:
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 distance = (global_position - Vector2(random_x,random_y)).length()
if distance >= distance_to_player:
isClose = false
else:
isClose = true
food_tscn.position = Vector2(random_x, random_y)
func change_score(food_tscn):
# 增加分数并且生成新的食物。
# create_food()这个方法随时都可以调用,这意味着我们可以用计时器调用,也可以为玩家创建技能生成食物来调用
player_score += food_tscn.value
score.text = "SCORE: " + str(player_score)
func over_game():
create_food_timer.stop()# 停止计时器
restart_button.text = "ReStart"
restart_button.show()
line_2d.clear_points()
set_physics_process(false)
position = Vector2(get_viewport().size.x/2,get_viewport().size.y/2)
get_tree().call_group("Food","queue_free")
func restart_game():
create_food_timer.start()
set_physics_process(true)
randomize() #randomize()是用来修改随机种子数
player_score = 0
score.text = "SCORE: " + str(player_score)
player_length = 4
player_size = color_rect.rect_size
player_type = TYPE[randi() % TYPE.size()] # 随机赋上一个属性
direction = Vector2(0,0)
set_sprite(player_sprite,player_type)
create_food()
func _on_EatArea2D_area_entered(area):
if area.name == "HurtArea2D":
var food = area.get_parent()
var food_type = food.food_type
if player_type == food_type:# 吃到相同属性变长
player_length += 1
elif player_type == eat_relation[food_type]:#如果玩家属性是食物克制的属性,死亡
over_game()
elif food_type == eat_relation[player_type]:#如果食物属性是玩家克制的属性,变成此属性然后变长
player_length += 1
player_type = food_type
set_sprite(player_sprite,food_type)
emit_signal("player_type_change")# 跟food说player的type改变,food是否要修改状态,变成静止或者追捕
func _on_Create_Food_Timer_timeout():
create_food()
func _on_RestartButton_pressed():
restart_game()
restart_button.hide()
Food.gd
extends KinematicBody2D
#吃掉食物的价值
var value
var food_type
var eat_relation = {"SCISSORS":"PAPER","ROCK":"SCISSORS","PAPER":"ROCK"}
var velocity = Vector2(0,0)
export var FRICTION = 2000
export var MAX_SPEED = 200
export var ACCELERATION = 2000
# 死亡发出信号,修改分数等操作
signal die
enum{IDLE,CHASE}
var catch_type = IDLE
var chaseArea2D_player = null # chaseArea2D区域中是否有player
func _physics_process(delta):
if catch_type == IDLE:# 变成静止,摩擦减速
velocity = velocity.move_toward(Vector2(0,0),FRICTION * delta)
elif catch_type == CHASE:
if chaseArea2D_player == null:
catch_type = IDLE
else:
# 获取指向玩家的向量
var direction = self.global_position.direction_to(chaseArea2D_player.global_position)
velocity = velocity.move_toward(direction * MAX_SPEED,ACCELERATION * delta)
velocity = move_and_slide(velocity)
# 信号传递,玩家状态变化时候,通知传递给每个食物
func _on_player_type_change(player):
# 一个bug
if player.player_type != eat_relation[food_type]:
catch_type = IDLE
else:
catch_type = CHASE
func _on_HurtArea2D_area_entered(area):
if area.name == "EatArea2D":
emit_signal("die")
queue_free()
func _on_ChaseArea2D_area_entered(area):
chaseArea2D_player = area.get_parent()
var player_type = chaseArea2D_player.player_type
if player_type == eat_relation[food_type]:# 变成追杀模式
catch_type = CHASE
func _on_ChaseArea2D_area_exited(area):
chaseArea2D_player = null
catch_type = IDLE
结语
此外,我们还可以将游戏的节奏进行修改,随时间逐步提升(移动速度、计时器生成食物间隔、食物生成距离范围等);也可以让地图在某个时刻变大或者有随机性,Boss等决定性关卡怪物的策划可以让游戏具有剧情性和更高的可玩性;而且声音是可以弄进去的。那就等后面再来为大家展示啦~(*^▽^*)
这次的教程到此结束了,后面可能会继续写新的教程文章展示给大家,感谢大家的观看!(#^.^#)