当贪吃蛇碰上剪刀石头布——godot版(用line2D)最终版!

本文基于Godot游戏引擎,详细介绍了剪刀石头布贪吃蛇游戏的制作过程。包括代码书写前的项目建设、添加属性、实现玩家与属性逻辑、构建墙壁区域、修改细节问题及总结bug等内容,还提及后续可对游戏节奏、地图等进行优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1、代码书写前的项目建设

2、添加三种属性:剪刀石头布

3、玩家和三种属性之间的逻辑实现

玩家相同属性

玩家被克属性

玩家克制属性

4、墙壁区域的构建(修改玩家移动)

5、一些问题细节的修改,让你的游戏看起来好看

食物重叠

距离玩家太近

6、bug的总结

结语


前提,先看完这篇文章:当贪吃蛇碰上剪刀石头布——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

玩家被克属性

若是玩家被克制的属性,主要是对食物代码的修改。

  1. 一是要对探测区进行处理:进入食物探测区,食物加速到最大速度然后进行追捕。
  2. 二是在追捕过程,若玩家修改了属性,则摩擦减速并且静止;若触碰到玩家,则玩家死亡,游戏结束。

我们先为食物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等决定性关卡怪物的策划可以让游戏具有剧情性和更高的可玩性;而且声音是可以弄进去的。那就等后面再来为大家展示啦~(*^▽^*)

        这次的教程到此结束了,后面可能会继续写新的教程文章展示给大家,感谢大家的观看!(#^.^#)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值