Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏(下)

godot_cover.jpg

一、前言

继续前面的两篇文章,《Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏》一共分为三小篇,链接如下:

主要内容:分析并制作一个完整的小游戏(下篇)
阅读时间: 6 分钟
永久链接: http://liuqingwen.me/blog/2018/12/06/introduction-of-godot-3-part-10-introduce-some-node-types-and-make-a-new-game-part-3/
系列主页: http://liuqingwen.me/blog/tags/Godot/

二、正文

本篇目标

  1. 了解学习游戏中的几个主要场景的制作
  2. 编写实现游戏中相关逻辑的代码
  3. 分析整个项目的一个开发流程

主要的场景

请参考上一篇:Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏(中)

代码与逻辑

部分代码见上篇文章:Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏(中)

相关的细节解释参考:Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏(上)

接下来是 UI 控件场景和 Main 游戏主场景的脚本代码,相对来说比较长,但是不难理解,相关重要的地方我已经做了注释,相信您能一目十行。 :grin:

5. UI.gd

extends Control

# 开始游戏的信号
signal start_game()

onready var _labelScore = $MarginContainer/HBoxContainer/LabelScore
onready var _labelTime = $MarginContainer/HBoxContainer/LabelTime
onready var _labelMessage = $VBoxContainer/LabelMessage
onready var _labelReady = $VBoxContainer/LabelReady
onready var _buttonStart = $MarginContainer2/ButtonStart

# 当前游戏是否被暂停,初始为“是”
var _isPaused = true

# 监听用户的输入
func _input(event):
    if event.is_action_pressed('start'):
        # 这个if条件语句只会在游戏开始时运行一次!
        if self.get_tree().paused != _isPaused:
            self.emit_signal('start_game')

        _isPaused = ! _isPaused
        self.get_tree().paused = _isPaused
        if _isPaused:
            _labelMessage.visible = true
            _labelMessage.text = 'Paused'
        else:
            _labelMessage.visible = false
            _buttonStart.visible = false

# 开始游戏按钮被按下
func _on_ButtonStart_pressed():
    _isPaused = false
    _labelMessage.visible = false
    _buttonStart.visible = false
    self.emit_signal('start_game')

# 显示Ready和目标金币数文本
func displayReady(target = 0, display = false):
    _labelReady.text = '%d, Ready!' % target
    _labelReady.visible = display

# 游戏结束显示的信息
func showGameOver():
    _isPaused = true
    _labelMessage.text = 'Game Over'
    _labelMessage.visible = true
    _buttonStart.text = 'Restart'
    _buttonStart.visible = true

# 显示分数(金币个数)
func showScore(score):
    _labelScore.text = str(score)

# 显示时间(剩余时间)
func showTime(time):
    _labelTime.text = str(time)

UI 子场景代码稍复杂,不仅要显示一些文字信息,比如当前时间、收集到的金币数等,还负责接收响应玩家的键盘输入,处理开始、暂停以及游戏重试等。当然,逻辑并不复杂。

唯一要注意的地方是 if self.get_tree().paused != _isPaused: 这个判断语句,我在代码中已经作了相关说明,它的判断结果只有在游戏开始运行的第一次时为 true ,其他任何时间都为 false (因为 _isPaused 的初始值的原因),也就是表示在开始游戏的时候玩家按了 start 按键(我在 Input Map 中设置 start 输入为空格和回车),然后发射游戏开始的信号。当然,你完全可以再定义一个变量来实现游戏的开始和暂停等。

6. Game.gd

extends Node2D

export(PackedScene) var coinScene = null
export(PackedScene) var powerScene = null
export(float) var minPlayerDist = 80
export(float) var minObstacleDist = 120

onready var _player = $Player
onready var _startPosition = _player.position
onready var _ui = $HUD/UI
onready var _pointsCurve = $CactusPoints.curve
onready var _cactus = $CactusPoints/Cactus
onready var _coinContainer = $CoinContainer
onready var _countTimer = $CountTimer
onready var _powerTimer = $PowerTimer
onready var _gameOverAudioPlayer = $GameOverAudio
onready var _levelAudioPlayer = $LevelUpAuido

var _level = 0 # 当前关卡
var _timeLeft = 0 # 剩余时间
var _totalCoins = 0 # 金币总数
var _collectedCoins = 0 # 收集金币数

func _ready():
    randomize() # 保证每次游戏都随机
    _player.isControllable = false

# 游戏结束初始化某些变量
func _gameOver():
    _level = 0
    _countTimer.stop()
    _ui.showGameOver()
    for coin in _coinContainer.get_children():
        coin.queue_free()

# 重新开始游戏调用方法
func _restartGame():
    _player.isControllable = false
    _totalCoins = _calculateTotal(_level)
    _timeLeft = _calculateDuration(_level)
    _collectedCoins = 0
    _ui.showScore(_collectedCoins)
    _ui.showTime(_timeLeft)
    _spawnObstacles()
    _spawnCoins()
    _player.restart(_startPosition)

    _ui.displayReady(_totalCoins, true)
    # 关键代码,如果不明白可以参考后面的解释
    yield(self.get_tree().create_timer(1.5, false), "timeout")
    _ui.displayReady()
    _player.isControllable = true
    _countTimer.start()
    _spawnPowerup()

# 进入下一关卡
func _nextLevel():
    _level += 1
    _restartGame()

# 玩家收集金币发出的信号处理
func _on_Player_coin_collected(count):
    _ui.showScore(count)
    if count >= _totalCoins:
        _countTimer.stop()
        _levelAudioPlayer.play()
        _nextLevel()

# 玩家受到伤害,游戏结束信号处理
func _on_Player_game_over():
    _gameOver()

# 玩家收集到能量币发出的信号处理
func _on_Player_power_collected(buffer):
    _timeLeft += buffer
    _ui.showTime(_timeLeft)

# 游戏时间超时,游戏结束
func _on_Timer_timeout():
    _timeLeft -= 1
    _ui.showTime(_timeLeft)
    if _timeLeft <= 0:
        _player.isControllable = false
        _gameOverAudioPlayer.play()
        _gameOver()

# 能量币定时生产
func _on_PowerTimer_timeout():
    var power = powerScene.instance()
    var pos = _makeRandomPosition()
    power.position = pos
    self.add_child(power)

# UI界面点击开始按钮触发开始信号
func _on_UI_start_game():
    _nextLevel()

# 创建当前关卡的所有金币
func _spawnCoins():
    if coinScene == null:
        return
    var playerPos = _player.position
    var obstaclePos = _cactus.position
    for i in range(_totalCoins):
        var coin = coinScene.instance()
        var pos = _makeRandomPosition()
        # 如果金币产生位置在玩家或者障碍物内,则重新生成一个位置
        while pos.distance_to(playerPos) < minPlayerDist || pos.distance_to(obstaclePos) < minObstacleDist:
            pos = _makeRandomPosition()
        coin.position = pos
        _coinContainer.add_child(coin)

# 设置当前关卡的障碍物置
func _spawnObstacles():
    var index = randi() % _pointsCurve.get_point_count()
    var position = _pointsCurve.get_point_position(index)
    _cactus.position = position

# 设置能量币出现的时间并计时
func _spawnPowerup():
    var powerTime = _makeRandomPowerAppearTime(_timeLeft)
    _powerTimer.wait_time = powerTime
    _powerTimer.start()

# 根据当前关卡设计金币总数
func _calculateTotal(level):
    return level + 5

# 根据当前关卡设计超时时长
func _calculateDuration(level):
    return level + 5

# 当前时间下设计随机能量出现时间
func _makeRandomPowerAppearTime(timeLeft):
    return rand_range(0, timeLeft)

# 根据窗口尺寸设计随机金币位置
func _makeRandomPosition():
    var x = rand_range(0, ProjectSettings.get('display/window/size/width'))
    var y = rand_range(0, ProjectSettings.get('display/window/size/height'))
    return Vector2(x, y)

嗯,这代码有点!当然,这是这个小游戏的核心代码部分了。 Game.gd 脚本把主场景中所有的子节点都相互关联在一起,让每个子场景相互配合,工作得有条不紊,另外它还会动态地创建一些其他的子节点,比如金币、能量币等。

代码中的主要逻辑在于处理游戏的开始、暂停、进入下一关卡以及结束等逻辑。对于每个关卡的元素合理设计,比如当前关卡的金币总数、超时时间、能量币的出现时机设计等,我没怎么用心,算法不是很合理,如果大家有兴趣,完全可以发挥自己的创造力丰富一下游戏的可玩性吧!嘿嘿。

其他需要注意的代码我在这里列出来:

  • randomize() 这个方法只需调用一次就可以在每次游戏运行时产生真实的随机效果
  • for coin in _coinContainer.get_children(): 获取该节点的所有子节点(金币)
  • self.get_tree().create_timer(1.5, false) 创建一个计时器,关键在 false 这个参数,表示场景暂停计时同步暂停
  • var position = _pointsCurve.get_point_position(index) 获取 Path2D 节点曲线上的某个点的位置值

关于 yield 关键字可以在上一篇文章中查看。最后运行游戏,进行测试吧! :smile:

游戏运行最终效果一览

三、总结

嗯,这个不好玩的小游戏总算完成了,总结一下我们的内容:

  1. 学习了一些新的 Godot 节点,以及一些新的关键词
  2. 探讨了一些基本的游戏开发规则,包括编写代码的规范
  3. 编写实现游戏中相关逻辑代码,完成我们第一个完整的小游戏

本次小项目以及相关的代码已经上传到 Github ,地址: https://github.com/spkingr/Godot-Demos原创不易,希望大家喜欢吧! :smile:

我的博客地址: http://liuqingwen.me ,欢迎关注我的微信公众号:

IT自学不成才

推荐阅读更多精彩内容