话说我正在开发一个SLG游戏,由于种种原因新换了个Godot引擎,既然是边学边用,不如把用godot实现个SLG的大致过程粗略记录下来,说不定对别人或未来的自己能派上点用处,岂不美哉?
所以本系列文章的主要内容是尝试godot引擎的一些功能(或许夹杂些程序架构和游戏设计思路吧)。只表示我个人是怎么做的,有什么不对的地方请多批评指教!
由于不完全是从零开始,对接引擎用的是C++语言(似乎不是很常规…),相关基础知识之前另个账号的日志里有。
最初,先来个HelloWor...还是先做个NewGame按钮吧,按下便开启新世界的大门!
搭建如下场景:
(具体怎么搭场景可以看官方文档的带入门教程)
(布局位置什么的可以后期打磨时再调)其中new_game_button的当前声明如下:
class game_button : public Button { GDCLASS(game_button, Button); protected: void pressed() override; public: std::function<void(void)> _on_pressed;// = [](){}; }; class new_game_button : public game_button { GDCLASS(new_game_button, game_button); public: new_game_button(); };
new_game_button足够特殊可以单独衍生出个子类来并注册进编辑器使用,不过每用个新功能按钮都这么写一遍可受不了,所以分出的game_button是更通用的,其public的_on_pressed可供利用。(使用例后续有)
实现如下:
void game_button::pressed() { Button::pressed(); _on_pressed(); } new_game_button::new_game_button() { _on_pressed = [this]() { //参照SceneTree::change_scene() Ref<PackedScene> p_scene = ResourceLoader::load("res://game_scene.tscn"); assert(!p_scene.is_null()); //参照SceneTree::change_scene_to() assert(p_scene.is_valid()); Node* new_scene = p_scene->instance(); get_tree()->call_deferred("_change_scene", new_scene); static_cast<game_scene*>(new_scene)->init("level_1"); }; }
被重载的虚函数game_button::pressed()在按钮被点击时调用,从而执行我们需要的功能。在new_game_button的构造函数中赋值为点下后切换场景到游戏主场景的lambda函数(主场景保存名为game_scene.tscn)。
对于场景切换函数的说明:我是很想简单的用SceneTree::change_scene(),但这个函数并不返回新场景的指针,而且通过翻阅源代码得知这个函数切换场景可能是延迟调用的,SceneTree::get_current_scene()估计也不会返回新场景。所以就有样学样的参照源码自己写了一遍函数内容,为的是得到新场景的指针,并调用其设置函数static_cast<game_scene*>(new_scene)->init("level_1")。
这样的好处是:点NewGame可以init("level_1"),点选关或者读档时可以init("level_X"),完全的数据驱动,不管以后游戏有几关,游戏关卡场景代码只写一遍,DRY(Don't repeat yourself)
这个game_scene也是自定义并注册到编辑器的Node,不过在这之前可以先看一下游戏场景结构:
回合制嘛,当前就摆了个回合数标签和回合结束按钮这一丁点。
实际上game_scene是我实现的一个适配器模式,用来包装引擎相关代码,只要它的接口不变,我以前写的那些引擎无关代码就可以不怎么做修改的接着使用。接口大致的感觉是这样的:
class game_scene : public Node { GDCLASS(game_scene, Node); public: void init(const string& file_name); void change_turn(uint32_t); void start_animate_turn_end_button(); void stop_animate_turn_end_button(); void disable_buttons(); void enable_buttons(); private: game_scene() :__controller(new game_controller(this)) {} ~game_scene() { delete __controller; } game_controller* __controller; Label* _turn_label; game_button* _turn_end; };
上面代码里的这个game_controller类是一个外观模式的实现,引擎通过它与所有引擎无关的系统交互(子系统包括游戏规则、游戏对象数据结构等,而引擎主要负责视图、声音、输入接收、外部存储这些),同时它也是一个单例。看起来大致是这个样子的:
class game_controller : public singleton<game_controller> { public: void start(const level_builder& level); void save(const string& file_name) const; void command_move(); void command_attack(); void command_supply(); void command_reinforce(); void command_switch(); void command_sleep(); void on_animation_finished(); void end_players_turn_command(); game_controller(game_scene* scene) : _scene(*scene) {} private: game_scene& _scene; input_state* _curr_state; }; #define __game game_controller::instance()
那么它们都做些什么事呢,以此为例
void game_scene::init(const string& file_name) { _turn_label = static_cast<Label*>(get_node(NodePath("UserInterface/TurnLabel"))); _turn_end = static_cast<game_button*>(get_node(NodePath("UserInterface/turn_end_button"))); _turn_end->_on_pressed = bind(&game_controller::end_players_turn_command, &__game); //点击响应通用用法 __game.start(level_builder(file_name)); }
(考虑到以后存档的方式可能变化,抽象出个level_builder对象作为参数,这样不管以后关卡数据文件或存档文件的格式发生什么变化,相关改变都隔离在了level_builder里,不会影响到其他部分的代码)
当关卡设置完毕后,便可接受玩家操作,比如点击按钮之类的。这时,响应函数处理玩家操作,回调引擎代理接口函数将游戏变化反馈出来,例如
void game_scene::change_turn(uint32_t n) { _turn_label->set_text(String(tr("TURN")) + itos(n)); }
(关于这个tr("TURN"),稍后还会再说到)
如此,游戏便成了通过规则处理玩家输入并反馈结果的machine。(由此,玩家在自己操作交互的游玩过程中会产生些什么情感体验之类的要说开就海了去了)
另举一例,回合结束按钮在所有单位行动结束后会通过AnimationPlayer控制闪烁橙色的光,这也是对内部维护的一个可行动单位列表的状况反馈。(顺带一提这个列表还能实现让玩家选择“下一个”单位的功能,同时也供AI依次控制NPC)
话说回来,你可以能会发现前面有一些类似NEW_GAME、TURN_END、TURN这样的字符串,使用这些字符串作KEY,应用到了godot提供的一个比较方便使用的国际化功能。编辑器里和代码里被Object::tr()包裹的KEY都会被预设的相应语言文字替换,如果你在游戏项目目录下放一个.csv文件的话。像这样:
,en,zh,ja
TURN_END,Units Done,结束回合,終了
然后打开项目设置,添加上.csv旁边自动生成的.translation文件,如图:
但是仅仅这样做还不够,因为内置字体显示不出中文这样的字。(如果不知道是因为字体的原因,这里恐怕要抓瞎一阵子了)
而且有关字体好像没找到文档里有专门这么一个章节……尤其是我想一次性设个全局字体。于是凭感觉找,看到 项目-项目设置-常规-Gui-Theme-CustomFont 这个设置有点像,需要.tres类型文件。之后下载了NotoCJK字体放进项目文件夹,在编辑器的文件系统里右键新建资源,看着DynamicFont这个名字应该是,双击新建的new_dynamicfont.tres,在属性面板里Font-FontData里加载下载的字体文件,回到项目设置里填上.tres文件。有效果,好像真的琢磨对了……
突然结束!今天就写到这里了。这个是预计十篇中的一篇,有了起始一步与大致结构,下一篇将是创建游戏空间。(以后日志可能会写的稍微简单点。。(趴
感谢大神,希望坚持下去,跟着学习一下
@Nitch:多谢 尽量坚持