C/C++程序员如何理解Godot的场景式游戏开发逻辑的?
阅读和写作背景
本人之前只接触过c/c++/python等传统串行程序,此前从未接触过任何游戏引擎(Unity/虚幻5等都没有接触过)。
本意想尝试自己做做游戏,因为Godot完全开源免费,因此第一意向就是Godot。
很小白,第一次接触Godot,照着Godot官网进行初步学习,学习到Godot关键概念概述的时候,我是懵逼的,我是不知所措的,有很多疑惑。在Godot游戏编辑器上创建的项目能够运行并且显示图片,但是我依然一头雾水,自觉脑袋很乱。如果你也有以下的疑惑,可以在本文中找找灵感:
- Godot项目的程序入口在哪儿?
- c/c++中有main程序入口,python的话直接运行哪个.py就是直接执行哪个.py的代码,很清晰。
- 这个Godot项目到底是如何设置主要程序的呢?
- 什么是场景/场景树?
- 这些场景/场景树在游戏过程中怎么进入/使用/退出的?
- 场景/场景树可以用面向对象的什么方式等效看待呢?
- 等等疑惑
那么开始从“C++ 等传统串行程序思维”转向Godot这种“场景树 + 信号驱动 + 生命周期框架”的思维模式吧。
传统串行程序实现游戏的基本思路
假如想要实现一个简单的游戏例子:玩家角色可以通过↑↓←→移动,按[空格]可以对敌人造成20点的hp伤害,敌人满血100点,当血量降为0的时候消失。
总结下就是以下的要点:
- 玩家角色
- 贴图显示
- 玩家移动控制
- 敌人
- 贴图显示
- hp值
- 玩家角色和敌人之间的互动(攻击等)
那么我们自然而然可以想到如下的基本结构体
// 角色基类
class CharacterBody2D {
public:
CharacterBody2D(Texture2D img, Vector2 position): _img(img), _position(postition) {};
void process(float delta) {}; // 每帧调用函数
void draw() {draw(_img)}; // 使用_img这个贴图进行图像显示
private:
Texture2D _img;
Vector2 _position;
int _speed = 200;
};
// 玩家角色
class Player: public CharacterBody2D {
public:
void process(float delta){
auto dir = Vector2.ZERO;
// 玩家控制方向移动角色
if (is_action_pressed('→')){
dir.x += 1;
}
if (is_action_pressed('←')){
dir.x -= 1;
}
if (is_action_pressed('↑')){
dir.y -= 1;
}
if (is_action_pressed('↓')){
dir.y += 1;
}
Vector2 velocity = dir.normalized() * _speed;
// 根据速度和时间移动本身
move_and_slide(delta, velocity);
}
void move_and_slide(float delta, Vector2 velocity){
if (velocity != Vector2(0,0)) {
_position += velocity * delta;
}
}
private:
int _speed = 200;
}
// 敌人角色
class Enemy: public CharacterBody2D {
public:
void process(float delta){
auto dir = Vector2.ZERO;
// 玩家控制方向移动角色
if (is_action_pressed('→')){
dir.x += 1;
}
if (is_action_pressed('←')){
dir.x -= 1;
}
if (is_action_pressed('↑')){
dir.y -= 1;
}
if (is_action_pressed('↓')){
dir.y += 1;
}
Vector2 velocity = dir.normalized() * _speed;
// 根据速度和时间移动本身
move_and_slide(delta, velocity);
}
void take_damage(int amount){
_health -= amount; // 被攻击后更新自身血量
}
bool should_disappear() {
return _health <= 0;// 用于判断是否应该消失
}
void draw() {
CharacterBody2D::draw();
draw_str(str(_health));
}
private:
int _health = 100; // 血量
}
当我们有了上述的玩家角色Player和敌人Enemy这两种类,为了游戏主逻辑清晰简单,可以使用下面的类进行整体的流程的封装:
class MainScene {
public:
MainScene() {
_player = Player();
_enemy = Enemy();
};
void process(float delta) {
_player.process(delta);
_enemy.process(delta);
};
void draw(){
_player.draw();
if (!_enemy.should_disappear()){
_enemy.draw();
}
}
private:
Player _player;
Enemy _enemy;
}
最后,我们按照游戏主循环编写,流程如下:
int main(){
MainScene Game = MainScene(); // 初始化游戏,加载纹理资源、必要的游戏对象
while(true) {
Game.process(); // 处理输入,同时更新事件等
Game.draw(); // 绘制每一帧内容
}
return 0;
}
Godot实现游戏的基本思路
还是以上面所描述的游戏为例,在Godot中到底又是如何构建起这个小游戏的呢?依然采取自下而上的方式进行设计:
玩家场景Player
- tscn文件:
- 类型:CharachterBody2D
- 依赖额外资源:
- Texture2D: player.png贴图文件
- Script: player.gd控制脚本
- 依赖子资源:
- RectangleShape2D:用于定义碰撞体积
- 成员对象实例:
- Sprite2D:贴图对象
- CollisionShape2D:碰撞对象
[gd_scene load_steps=4 format=3 uid="uid://dcjlwlvhmti0k"]
[ext_resource type="Texture2D" uid="uid://cag1qcbgl437k" path="res://asserts/player.png" id="1_4flbx"]
[ext_resource type="Script" uid="uid://dhf8rfgovq2s1" path="res://player.gd" id="1_onrkg"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_onrkg"]
[node name="CharacterBody2D" type="CharacterBody2D"]
script = ExtResource("1_onrkg")
[node name="Sprite2D" type="Sprite2D" parent="."]
position = Vector2(300, 300)
scale = Vector2(0.2, 0.2)
texture = ExtResource("1_4flbx")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(300, 300)
scale = Vector2(4, 4)
shape = SubResource("RectangleShape2D_onrkg")
- gdscript文件:
extends CharacterBody2D
## 定义执行对敌人攻击的信号
signal attack_enemy()
## 玩家角色移动速度
var speed: int = 200
func _process(_delta: float) -> void:
var dir := Vector2.ZERO
if Input.is_action_pressed("ui_right"):
dir.x += 1
if Input.is_action_pressed("ui_left"):
dir.x -= 1
if Input.is_action_pressed("ui_up"):
dir.y -= 1
if Input.is_action_pressed("ui_down"):
dir.y += 1
velocity = dir.normalized() * speed
move_and_slide()
func _input(event):
if event is InputEventKey and event.is_pressed() and event.keycode == KEY_SPACE:
emit_signal("attack_enemy")
敌人场景Enemy
- tscn文件:
- 类型:CharachterBody2D
- 依赖额外资源:
- Texture2D: enemy.png贴图文件
- Script: enemy.gd控制脚本
- 依赖子资源:
- RectangleShape2D:用于定义碰撞体积
- 成员对象实例:
- Sprite2D:贴图对象
- CollisionShape2D:碰撞对象
- Label: 显示血量
[gd_scene load_steps=4 format=3 uid="uid://bvorb11f3qd8v"]
[ext_resource type="Script" uid="uid://c2mtv5iqfmlpg" path="res://enemy.gd" id="1_4gyqm"]
[ext_resource type="Texture2D" uid="uid://cyyfxt16mcyhu" path="res://asserts/enemy.png" id="1_7k104"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_7k104"]
[node name="Enemy" type="CharacterBody2D"]
script = ExtResource("1_4gyqm")
[node name="Sprite2D" type="Sprite2D" parent="."]
position = Vector2(700, 400)
scale = Vector2(0.2, 0.2)
texture = ExtResource("1_7k104")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(700, 400)
scale = Vector2(4, 4)
shape = SubResource("RectangleShape2D_7k104")
[node name="HPLabel" type="Label" parent="."]
offset_left = 677.0
offset_top = 282.0
offset_right = 717.0
offset_bottom = 305.0
text = "100"
horizontal_alignment = 1
- gdscript文件:
extends CharacterBody2D
var health := 100
@onready var hp_label = $HPLabel
func _ready() -> void:
update_hp_label()
func update_hp_label() -> void:
hp_label.text = str(health)
func take_damage(amount):
health -= amount
update_hp_label()
print("Enemy HP:", health)
if health <= 0:
queue_free()
主场景MainScene
现在,我们回到一开始的问题
- Godot的程序入口是什么?
Godot的程序入口就是在项目中设置的主场景
- 那么一个场景又代表什么呢?
一个场景代表可以复用的模块,这个模块包含了_ready/_input/_process/_exit等覆盖了游戏循环内外的各个主要任务的这些接口。
- 那么这个例子中我们又该如何理解场景树呢?
- 场景树由不同的场景经过实例化组成,而子场景由是由其他Node或者场景实例化组合而成的。
组合是Godot复用的关键功能。
- 场景树由不同的场景经过实例化组成,而子场景由是由其他Node或者场景实例化组合而成的。
那么来看下这个小游戏的Godot表现效果吧。

那么最终串行程序跟Godot之间的核心对照表是如何的呢?
| 概念 | C++ 程序 | Godot |
|---|---|---|
| 主入口 | main() 函数 | 主场景 (MainScene.tscn) |
| 对象关系 | 成员变量嵌套 | 节点树嵌套 |
| 生命周期 | 构造/析构函数 | _ready() / _exit_tree() |
| 逻辑更新 | process() 主循环 | _process(delta) 引擎回调 |
| 输入检测 | 轮询 handle_input() | _input(event) 自动回调 |
| 通信方式 | 函数调用 / 引用对象 | 信号 (signal-slot) |
| 资源加载 | 手动 load 文件 | preload() 自动管理 |
| 销毁 | delete obj; | queue_free() |
| 程序控制权 | 你拥有主循环 | Godot 拥有主循环 |
1523

被折叠的 条评论
为什么被折叠?



