C/C++程序员如何理解Godot的场景式游戏开发逻辑的?

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复用的关键功能
MainScene
PlayerScene
EnemyScene
MainScene
Sprite2D
EnemyScene
CollisionShape2D
Label
Sprite2D
PlayerScene
CollisionShape2D

那么来看下这个小游戏的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 拥有主循环
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值