Godot实战-贪吃蛇(7)

【开始界面和场景切换】

游戏需要一个开始界面。我希望在点击开始按钮之后,跳转到关卡设置界面,完成设置之后再跳转到真正的关卡界面。

【开始和设置界面】

  1. 新建一个场景,根节点设置为CanvasLayer,重命名为Welcome。为Welcome结点新增一个Sprite结点,重命名为Background;新增一个Button结点,重命名为ButtonStart。保存场景为welcome.tscn。
    请添加图片描述

  2. 打开PS,新建一个1152x648(px)的画布(大小和项目设置里的Window大小相同)。用油漆桶随便涂一个背景色,保存psd文件,并另存为bg.jpg。将bg.jpg赋值给Background结点的Texture。为了让Background填满镜头,该结点的position应设置为x=576,y=324(图片宽、高的一半)。

  3. 点击ButtonStart结点,将text更改为“开始游戏”,FontSize设置为80,Anchor Preset设置为Center。
    请添加图片描述

    这是现在的开始界面。记得保存场景。

  4. 新建一个场景,根节点设置为CanvasLayer,重命名为ConfigPage。用同样的设置添加Background。保存为config_page.tscn。

  5. 目前,我希望设置的是敌人的数量和每条蛇的颜色。添加三个Label并在ThemeOverrides里把Font Size改成40。修改text并改变位置来探索合适的布局。
    请添加图片描述
    请添加图片描述

  6. 数量和颜色都可以用下拉列表来选择。新增一个OptionButton结点,重命名为NumberOption,Font Size同样设置为40。在Inspector的Items中添加3个新元素,text分别设置为1、2、3,其他参数不用动。拖动该结点到“敌人数量:”旁边,现在运行该场景,会发现已选择的数字很大,提供的选项中数字却很小。
    请添加图片描述

  7. OptionButton的弹出选项实际上是PopupMenu结点。在OptionButton中无法直接更改它子结点的样式,需要设置Theme(主题)来更改。在NumberOption结点的Inspector中找到Theme,点击New Theme创建新主题。
    请添加图片描述

    之后点击Theme旁边的彩虹水滴,在下方出现的Theme选项卡中开始编辑新的主题。
    请添加图片描述

  8. 首先点击最右边的加号,找到PopupMenu。之后就可以针对这种结点进行专门的样式修改。
    请添加图片描述

    目前我只关心字号。点击两个T重叠的选项卡,点击font_size旁边的加号,然后修改字体大小为32。
    请添加图片描述

    现在运行场景,显示的大小已经比较合适了。
    请添加图片描述

  9. 复制NumberOption结点,重命名为ColorOption,拖动到合适的位置。将Items改成红、黄、蓝,就可以得到类似的效果。
    请添加图片描述

  10. 这样编辑颜色选项既不直观,也不方便维护。最好是使用代码来直接填充色块作为选项。点击右边的垃圾桶图标将ColorOption中的所有Item全部删除,然后为ConfigPage新增脚本config_page.gd。

extends CanvasLayer

@export var colors : PackedColorArray

func _ready():
	var img = Image.new().create(30, 30, true, Image.FORMAT_RGB8) #创建30x30的图片
	var texture : Texture2D
	
	for i in range(colors.size()):
		img.fill(colors[i]) #填充图片为纯色,颜色为colors[i]
		texture = ImageTexture.create_from_image(img)
		$ColorOption.add_icon_item(texture, "", i)  #texture, label, id

通过@export使得变量可以通过Inspector赋值和修改。点击ConfigPage结点,从Inspector-Colors中点击Add Element来逐个添加和修改颜色。
请添加图片描述

  1. 现在运行场景,可以很直观地选择颜色了。
    请添加图片描述

  2. 之后是布局调整,操作比较繁杂,故而略去细节。技术方面主要参考视频【【中音】Godot UI 基础-如何构建在任何地方都适用的漂亮界面】 ,要诀是善用各种Container,以及使用Control结点当空白占位。
    请添加图片描述

以上是参考的结点树,VBoxColorOpts是一个VBox,设置为唯一名称访问,用于挂颜色选择条。颜色选择条以HBox为根节点,有一个Label和一个OptionButton。

  1. 这是颜色选择条的场景结点树。ColorOpt设置为唯一名称访问,初始不包含任何item。
    请添加图片描述

为这个场景附加脚本。把之前写在config_page.gd里的代码剪切过来,并增加一个修改Label文字的方法。

extends HBoxContainer

@export var colors : PackedColorArray

func _ready():
	var img = Image.new().create(30, 30, true, Image.FORMAT_RGB8)
	var texture : Texture2D
	
	for i in range(colors.size()):
		img.fill(colors[i])
		texture = ImageTexture.create_from_image(img)
		%ColorOpt.add_icon_item(texture, "", i)

func label_update(newLabel:String):
	$Label.text = newLabel

在Inspector里可以像之前一样对Colors添加和修改颜色。记得保存。

  1. 现在,通过代码在ConfigPage场景里实例化颜色选择条。切换到config_page.gd:
extends CanvasLayer

var color_opt_scene : PackedScene = preload("res://Scenes/hbox_color_opt.tscn")

func _ready():
	var player_color_opt = color_opt_scene.instantiate()
	player_color_opt.label_update("玩家 ")
	%VBoxColorOpts.add_child(player_color_opt)
	
	for i in range(3):
		var enemy_color_opt = color_opt_scene.instantiate()
		enemy_color_opt.label_update("敌人"+str(i+1))
		%VBoxColorOpts.add_child(enemy_color_opt)
  1. 运行场景,可以看到一个相对整齐的布局和4个颜色选择条,对应玩家和三个敌人。之后可以让颜色条的数量和敌人数量对应起来。
    请添加图片描述

【连接场景和传递参数】

现在,需要把开始界面、设置界面和关卡连接起来。

  1. 回到Welcome场景,将ButtonStart的pressed信号连接到Welcome结点。打开welcome.gd,编辑相应的方法。
func _on_button_start_pressed():
	get_tree().change_scene_to_file("res://Scenes/config_page.tscn")

change_scene_to_file()方法可以从当前场景跳转到指定场景,参数是场景文件的路径。
运行Welcome场景,点击“开始游戏”按钮后,将跳转到设置界面。

  1. 为ConfigPage场景新增两个按钮ButtonContinue和ButtonBack,FontSize均设置为40,布局过程省略。将这两个按钮的pressed信号连接到ConfigPage结点。

  2. 考虑在设置界面会有哪些变量:敌人数量,还有每条蛇的颜色。
    var enemy_num : int = 1
    var color_list = []

  3. 找到敌人数量下拉选框NumberOpt,连接信号item_selected到ConfigPage结点。每当选择了新的数字,就会发射这个信号。在config_page.gd里切换

func _on_number_opt_item_selected(index):
	# 每次选择后,先清空所有敌人的颜色选择条
	for i in range(1, %VBoxColorOpts.get_child_count()):
		%VBoxColorOpts.get_child(i).queue_free()
	
	# 根据选择的项修改enemy_num,并添加对应数量的颜色选择条
	enemy_num = int(%NumberOpt.get_item_text(index))
	for i in range(enemy_num):
		var enemy_color_opt = color_opt_scene.instantiate()
		enemy_color_opt.label_update("敌人"+str(i+1))
		%VBoxColorOpts.add_child(enemy_color_opt)

如果不做清空操作,在多次改变数量选项后敌人的颜色选择条将不断增殖。初始状况,enemy_num默认为1,应该显示一个敌人的颜色选择条。同时还有玩家的选择条。所以_ready()方法应该是这样的:

func _ready():
	var player_color_opt = color_opt_scene.instantiate()
	player_color_opt.label_update("玩家 ")
	%VBoxColorOpts.add_child(player_color_opt)
	
	# 预设一个敌人
	var enemy_color_opt = color_opt_scene.instantiate()
	enemy_color_opt.label_update("敌人1")
	%VBoxColorOpts.add_child(enemy_color_opt)
  1. 运行场景,改变敌人数量时,旁边的颜色选择条也会跟着变。
    请添加图片描述

  2. 现在考虑如何将选择的颜色存储起来。实际上,并不需要追踪每一次选择修改,只要在点击确认时记录那一瞬间的选择即可。

    在hbox_color_opt.gd(颜色选择条的脚本)中,增添一个获取所选icon颜色的方法:

func get_selected_color() -> Color:
	var id = %ColorOpt.get_selected_id()
	var icon : Texture2D = %ColorOpt.get_item_icon(id)
	return icon.get_image().get_pixel(0,0)

get_pixel()会返回指定坐标的像素的颜色,以(R, G, B, A)的形式。

  1. 在config_page.gd中设置点击ButtonContinue之后的操作。
func _on_button_continue_pressed():
	for c in %VBoxColorOpts.get_children():
		color_list.append(c.get_selected_color())
	
	print(color_list)
	color_list = [] #测试用,避免多次点击按钮后记录的颜色不断增加

现在运行场景。在初始状态点击“确认!”会得到两个红色。改成三个敌人然后分别改变颜色选择,在点击“确认!”后也会正确地得到每个所选的颜色。
请添加图片描述

  1. 关于在切换场景时传参,目前我没有找到合适的方法。一个替代方案是使用全局脚本,创建global.gd,然后把设置信息放在里面。
extends Node

var enemy_num : int = 1
var color_list = []

之后,通过Project Settings-Autoload,点击白色文件夹图标找到global.gd并添加,确保Enable是被勾选的,然后确认。现在global.gd是一个全局脚本,可以在任何地方访问。
请添加图片描述

  1. 回到config_page.gd。在点击“确认!”按钮时,修改两个全局变量并切换场景到Level:
func _on_button_continue_pressed():
	Global.enemy_num = int(%NumberOpt.get_item_text(%NumberOpt.get_selected_id()))
	
	for c in %VBoxColorOpts.get_children():
		Global.color_list.append(c.get_selected_color())
	
	get_tree().change_scene_to_file("res://Scenes/level.tscn")

点击“返回”按钮时,只需要简单地回到开始界面。

func _on_button_back_pressed():
	get_tree().change_scene_to_file("res://Scenes/welcome.tscn")

现在运行游戏,可以看到两个按钮都能正确地切换场景。

【将设置的数值应用于关卡】

现在有两个关卡需要使用的设置数值:敌人数量,和所有蛇的颜色列表。前一个很容易使用,只要在初始化敌人时for i in range(Global.enemy_num):即可。对于后一个,首先要让蛇能够改变颜色。

【蛇的调色】

之前在测试时为了区分玩家和敌人,对敌方蛇的蛇头进行了调色(modulate)。现在要做的就是将调色应用于整条蛇。

  1. 首先来到snake.gd,增添一个变量var color : Color = Color.BLUE_VIOLET
    _ready()中,初始化蛇头的地方,添一句$SnakeHead.modulate = color
    add_body()中,body加入场景树之前,添一句body.modulate = color
    最后,删除enemy.gd的_ready()$SnakeHead.modulate = Color.CADET_BLUE这句。

    我选择了一个比较醒目的默认颜色,这样更容易分辨。现在运行Level场景,能看到紫色的蛇……呃,有点恶心。
    请添加图片描述

  2. 把默认颜色改成WHITE。回到level.gd,在create_snake()方法里加一句snake.color = Global.color_list[id],需要写在snake加入场景树之前。

    另外,为了不展示出空白的Panel,从场景树中将Panel1-3隐藏,然后在初始化敌人时展示对应的Panel:$UI/Panels.get_child(i+1).show()
    请添加图片描述

    现在初始化敌人部分的代码应该是这样的:

	# 创建敌人
	for i in range(Global.enemy_num):
		var enemy = create_snake(true, i+1)
		snake_list.append(enemy)
		var board = board_scene.instantiate()
		$UI/Panels.get_child(i+1).show()
		$UI/Panels.get_child(i+1).add_child(board)

完整的level.gd代码如下:

extends Node2D

var food_scene : PackedScene = preload("res://Scenes/food.tscn")
var player_scene : PackedScene = preload("res://Scenes/player.tscn")
var enemy_scene : PackedScene = preload("res://Scenes/enemy.tscn")

var player : Snake
var snake_list = []

var game_duration : int = 300
var INIT_FOOD_NUM : int = 500


func _ready():
	# 初始化倒计时
	$Timer.wait_time = game_duration
	
	# 初始化食物
	generate_random_foods()
	
	# 初始化UI
	var board_scene = preload("res://Scenes/score_board.tscn")
	var player_board = board_scene.instantiate()
	$UI/Panels.get_child(0).add_child(player_board)
	
	# 创建玩家
	player = create_snake(false, 0)
	snake_list.append(player) #需要一个空玩家来占位
	
	# 创建敌人
	for i in range(Global.enemy_num):
		var enemy = create_snake(true, i+1)
		snake_list.append(enemy)
		var board = board_scene.instantiate()
		$UI/Panels.get_child(i+1).show()
		$UI/Panels.get_child(i+1).add_child(board)
	
	$UI.update_all(0) #开场分数清零
	$Timer.start()
	update_left_time() #每秒更新倒计时


func generate_random_foods():
	for i in range(500):
		var food = food_scene.instantiate()
		var tl = $MarkerTopLeft.position
		var br = $MarkerBottomRight.position
		food.position = Vector2(randi_range(tl.x, br.x), randi_range(tl.y, br.y))
		$Foods.add_child(food)


func create_snake(isEnemy:bool, id:int):
	var snake = enemy_scene.instantiate() if isEnemy else player_scene.instantiate()
	snake.id = id
	snake.isEnemy = isEnemy
	snake.color = Global.color_list[id]
	snake.global_position = $SpawnPoints.get_child(id).global_position
	add_child(snake)
	snake.score_changed.connect($UI.update_board)
	snake.burst_to_foods.connect(handle_burst)
	snake.died.connect(handle_death)
	return snake

func handle_death(isEnemy:bool, id:int):
	snake_list[id].queue_free()
	snake_list[id] = create_snake(isEnemy, id)

func handle_burst(pos_list : Array):
	for pos in pos_list:
		var food = food_scene.instantiate()
		$Foods.add_child(food)
		food.global_position = pos

func update_left_time():
	$UI.update_countdown(ceil($Timer.time_left))
	# 每当超时,调用自身
	get_tree().create_timer(1).timeout.connect(update_left_time) 

【从开始“开始”】

  1. 进入Project Settings-General-Application-Run,将Main Scene改成welcome.gd。

  2. 现在运行游戏(而不是运行场景!)。最初是欢迎界面,点击“开始游戏”将进入设置界面,可以在这里选择敌人的数量和每条蛇的颜色。选好之后点击“确认!”将进入关卡,看上去这已经像是一个游戏了。

请添加图片描述
请添加图片描述
请添加图片描述

【结束游戏】

当倒计时结束,我希望暂停当前场景,并根据分数给出排名。

分数位数太多影响布局,编写change_text方法,更改分数时根据位数决定字号。

  1. 先在Level场景中创建结束时的排名板:为UI结点新增一个PanelContainer,重命名为GameOverPanel。拖拽设置Panel为合适的大小,anchor设置为center。

  2. 为这个Panel新增一个VBoxContainer作为子结点,Layout-Container-Sizing设置为H方向Fill,V方向Shrink Begin。

  3. VBox中继续增加子结点,首先是一个Label,写“游戏结束”;然后是一个HBox,里面有三个label,分别是排名、名字和分数。再加上合适的空白占位,这样的HBox就可以作为模板表示一条Rank。最后大概是这样的效果:
    请添加图片描述

  4. 把含有三个Label的HBox重命名为HBoxRank,右键,Save Branch as Scene。打开hbox_rank.scene,附加脚本,加一个更新信息的方法。

func update_info(rank:int, refer:String, score:int):
	$RankLabel.text = str(rank)
	$NameLabel.text = refer
	$ScoreLabel.text = str(score)
  1. 回到Level场景,清空不需要的结点,现在GameOverPanel下面应该只有这些:
    请添加图片描述

  2. 点击倒计时用的Timer结点,将其time_out信号连接到Level,然后编写方法:

func _on_timer_timeout():
	get_tree().paused = true
	var rank_scene = preload("res://Scenes/hbox_rank.tscn")
	# 按分数排序
	snake_list.sort_custom(func(a:Snake, b:Snake):
		return true if a.score>b.score else false
	)
	
	$UI/GameOverPanel.show()  #展示结算面板
	$UI/Panels.hide()   #隐藏分数板

	# 生成分数条
	var i = 1
	for snake in snake_list:
		var rank_bar = rank_scene.instantiate()
		var refer = "敌人"+str(snake.id) if snake.isEnemy else "玩家"
		rank_bar.update_info(i, refer, snake.score)
		$UI/GameOverPanel/VBoxContainer.add_child(rank_bar)
		i += 1

运行游戏,在倒计时结束后,画面冻结,跳出结算界面。(测试时可以减少game_duration
请添加图片描述

【重玩和回到标题】

现在每次游戏结束,都必须关闭游戏再重进,这是不合理的。因此,需要“重玩本局”和“回到标题”。这里不需要太复杂,直接切换场景即可。注意这两个按钮只有结算面板跳出时才会出现,因此需要先解除pause状态。

  1. 在GameOverPanel-VBoxContainer下增添子结点HBox,重命名为HBoxButtons。在HBox中新增两个按钮,重命名为ButtonReset和ButtonBack。增加三个Control结点用于调整布局。
    请添加图片描述

  2. 连接ButtonReset的pressed信号到Level,编写方法:

func _on_button_reset_pressed():
	get_tree().paused = false
	get_tree().change_scene_to_file("res://Scenes/level.tscn")
  1. 连接ButtonReset的pressed信号到Level,编写方法:
func _on_button_back_pressed():
	get_tree().paused = false
	get_tree().change_scene_to_file("res://Scenes/welcome.tscn")
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值