Project1

标题: 设计一个简单的游戏引擎 [打印本页]

作者: guoxiaomi    时间: 2021-12-8 00:25
标题: 设计一个简单的游戏引擎
本帖最后由 guoxiaomi 于 2021-12-8 00:29 编辑

以下是我对游戏引擎的一些理解。

当我想到游戏引擎的各个组分时,很快脑子里就浮现出下面的画面:


一个引擎应该要有处理逻辑、画面、音乐等等部分,然后还要有数据池用来存放数据。为了提高并行度,可能还有任务池,让一些任务能在并行的执行。

提到并行,自然就会想到多线程了。比如,把逻辑、画面、音乐分别放到不同的线程里处理。这些线程会各自管理自己的独有数据,但是也可以被其他的线程所操作:

如果有的功能耗时很长需要异步处理,就需要一个任务队列。这样线程A可以给线程B“布置”任务,布置结束后就继续执行自己的。任务会加入到线程B的任务队列,会按照先来后到的顺序依次执行。

进一步抽象,一个线程需要的东西包括:1) 数据库 2) 任务队列 3) 任务的执行方式。再额外加上一个 4) 暂停标记,用来暂停、恢复线程的运行(有时候布置的任务需要等待其他的线程完成了才能继续):


这个时候,游戏引擎的主干框架就完成了。每个线程,无论是处理逻辑、画面、音乐还是网络,无非就是上面的1-3条有所不同,只需要设计好特定的数据类型、任务类型和执行方式,填入到这个框架里即可。比如:
线程功能数据库可执行的任务任务执行方式
逻辑游戏数据获取网络资源数据每1/60s:更新逻辑,处理队列任务,处理用户输入,呼叫渲染线程绘制画面
绘制图片资源加载资源、释放资源、绘制画面立刻执行队列里的任务
音乐音乐资源加载资源、释放资源、播放音乐立刻执行队列里的任务
网络网络数据访问网站、下载内容收到逻辑线程的下载任务通知,下载完毕后发送通知给逻辑线程


还有一点要注意的是,线程间的同步问题。当绘制线程在绘制画面时,可能要读取玩家在画面上的坐标,而这个数据其实是由逻辑线程保管的。所以可以这样处理:
1. 逻辑线程通知绘制线程可以开始干活
2. 逻辑线程暂停自己
3. 绘制线程访问逻辑线程的数据,绘制画面
4. 绘制线程恢复逻辑线程的运行

这就是前面提到的暂停标记的作用,它可以用来保证只有一个线程会访问数据。

这样的设计就足够简单了吧?接下给出这个主干框架的一个C++实现: asxp.zip (313.76 KB, 下载次数: 28)
内含main.exe是gcc编译的,需要C++20(感受模板元编程的威力吧)。这个C++实现的主干框架,最大的特色在于拥有所谓的零开销抽象,并且没有内存泄漏的问题(new操作符没出现)。

在设计好特定的数据类型、任务类型和执行方式后,只需要组合这些类型,告知每一个线程需要管理哪些数据、处理哪些任务以及具体的执行者,程序就完成了,只要40行代码:

main.h + main.cpp


作者: 89444640    时间: 2021-12-8 10:08
本帖最后由 89444640 于 2021-12-8 10:09 编辑

说起多线程和并行,我就想起一个问题,为什么更高级的制作软件他不卡,而rm并行处理,地图里只要多于一个必定会卡,而且地图越大卡的越明显,这还是走格子游戏,实际上处理已经降低了很多,而且地图一般也不会画满500*500,既然给了500*500的地图那就得做到这个大小情况下保证运行效率呀!如果是rm这个处理方式,按真实坐标判点断根本就没法运行了,这还得说xp刷新率只有40帧一般游戏都是60帧
比如一个很常见的NPC满大街跑,无论怎么绕都是躲避障碍物移动到某点然后消失,就是寻路,地图只要大那么一点,同时执行寻路的npc多那么一点,就卡的根本动不了,
还有一些动作游戏中常见的,躲避敌人的视线潜入据点,地图中肯定会有多个npc在巡逻,很多动作游戏该有的基础的东西,XP根本就没法实现╮(╯▽╰)╭
作者: xiaohuangdi    时间: 2021-12-8 10:53
作为引擎使用者,我觉得最重要的是效率吧。编辑器界面要简洁,能快速点击功能。
而不是技术力 更不是光效 3D 网游。

也就是分成很多个专用引擎,把制作的类型固定住,但是又让制作者能轻微换皮魔改的空间。

建议楼主先做一些小游戏的制作引擎, 比如 连连看,拼涩图,小说gal。
然后是横版闯关   

作者: Im剑侠客    时间: 2021-12-8 18:52
新runtime有XP的份吗?如果有务必要期待一番,前面出过的runtime对XP的支持并不是很好。
作者: guoxiaomi    时间: 2021-12-8 19:14
本帖最后由 guoxiaomi 于 2021-12-8 19:21 编辑
Im剑侠客 发表于 2021-12-8 18:52
新runtime有XP的份吗?如果有务必要期待一番,前面出过的runtime对XP的支持并不是很好。 ...


其实在 https://rpg.blue/thread-486547-4-1.html 的34楼时,已经把XP做的差不多了,就是有点卡(比原版还卡),不过卡顿的原因已经找到了。

我没有继续做的原因是程序设计太丑陋,所以去补了课(设计模式、C++元编程),用新学的知识重构了底层框架。接下来把原来做的功能挨个加入这个新的底层框架即可。
作者: guoxiaomi    时间: 2022-4-8 14:35
本帖最后由 guoxiaomi 于 2022-4-8 14:47 编辑

清明节前后有几天空闲时间,写好了引擎的核心框架: rgm_v0.1.2.zip (5.15 MB, 下载次数: 9) ,决定加入RG系列,命名为RGM,意为Modern RPG Maker。26个字母快不够用了

双击 main.exe 就会执行 main.rb 中的代码。核心代码在 src/core 里(模板元编程高能预警),思路跟主楼的区别不是很大,但是把SDL2和ruby都安排进来了。

这个核心框架有两个独立线程:逻辑线程(ruby)和渲染线程(SDL2),它们之间通过传递指令来交互,而后续的开发的主要工作就是堆上各种指令,这也算是一种模块化吧。

比如,下面的 test.hpp 中定义了2个指令
1. fill_red 会把整个画面涂成红色
2. call_fill_red 定义了内部类 wrapper,并绑定其静态函数 call_fill_red 到 ruby 中,成为 Test 模块的 fill_red 方法。调用 Test.fill_red 时会向渲染线程发送指令 fill_red。
3. 指令可以定义类型对象 data,指定其需要的数据类别。可以定义run方法代表具体的操作,以及静态方法before和after代表在线程开始和结束时的额外任务。这些都是可选的,比如 call_fill_red 就只有 before 方法。

  1. #include "core/core.hpp"

  2. struct fill_red
  3. {
  4.     using data = rgm::data<std::unique_ptr<cen::window>, std::unique_ptr<cen::renderer>>;

  5.     void run(rgm::worker auto* worker)
  6.     {
  7.         cen::window& window = *worker->template get<std::unique_ptr<cen::window>>();
  8.         cen::renderer& renderer = *worker->template get<std::unique_ptr<cen::renderer>>();

  9.         printf("fill %d x %d.\n", window.width(), window.height());
  10.         renderer.clear_with(cen::colors::red);
  11.         renderer.present();
  12.     }
  13. };

  14. struct bind_fill_red
  15. {
  16.     static void before(rgm::worker auto* worker)
  17.     {
  18.         static const decltype(worker) w{ worker };

  19.         struct wrapper
  20.         {
  21.             static VALUE call_fill_red(VALUE module)
  22.             {
  23.                 printf("call fill red.\n");
  24.                 fill_red a{};
  25.                 w->send(std::move(a));
  26.                 return Qnil;
  27.             }
  28.         };

  29.         VALUE rb_mTest = rb_define_module("Test");
  30.         rb_define_module_function(rb_mTest, "fill_red", wrapper::call_fill_red, 0);
  31.     }
  32. };


  33. namespace rgm::core
  34. {
  35. using list1_t = commandlist<bind_fill_red>;
  36. using list2_t = commandlist<fill_red>;

  37. using worker1_t = worker<commandlist<init_ruby, list1_t>, executor_ruby>;
  38. using worker2_t = worker<commandlist<init_sdl2, list2_t>, executor_sdl2>;

  39. using engine_t = scheduler<worker1_t, worker2_t>;

  40. template <>
  41. struct traits::magic_cast<scheduler<>*>
  42. {
  43.     using type = engine_t*;
  44. };
  45. }

  46. int main(int argc, char* argv[])
  47. {
  48.     printf("start rgm.\n");
  49.     cen::library centurion;
  50.     rgm::core::engine_t engine;
  51.     engine.run();
  52.     printf("exit rgm.\n");
  53.     return 0;
  54. }
复制代码


个人觉得,核心框架应该是最难写的部分了,后面的难点大概是渲染流程(Graphics.update)和shader的应用(Bitmap.hue_change和Tone)。
作者: guoxiaomi    时间: 2022-4-26 00:56
本帖最后由 guoxiaomi 于 2022-4-26 19:39 编辑

V0.2版本

已达成该版本的主要目标:完成渲染流程。到这一步,游戏引擎的架构已经很清晰了:多线程执行,隔离线程数据,线程间使用管道交换信息。
画面的绘制是异步的,表现在:
1. ruby线程里对Bitmap的创建、销毁和绘制等操作,都是发送相应指令到render线程异步执行。
- 这意味着对Bitmap的操作几乎是瞬间返回
- 由于指令有严格的顺序,可以保证在下一次画面绘制之前,所有的Bitmap的操作都已完成
- 在创建Bitmap时,会先快速读出图片的高和宽,供后续的ruby脚本调用

2. 画面绘制开始时,ruby线程会按z轴顺序遍历所有的viewport和sprite,向render线程发出相应的绘制指令,然后锁定自己,直到收到render线程的信号。
- 这样可以保证数据的一致性,从而render线程能正常访问属于ruby线程的数据
- 遍历红黑树、将ruby数据打包这些内容都是ruby线程负责,分担了render线程的工作量
- 绘制结束时render线程会先解锁ruby线程,然后等待垂直同步信号。也可以选择强制同步,先等待画面完成绘制后解锁ruby线程。


目前可以做以下事情:
1. 创建Bitmap,读取图片或者创建已知大小的空白图
2. 使用Bitmap#fill方法涂上某种颜色
3. 创建Sprite/Viewport,设置其z轴和位置等属性
4. Sprite支持设置bitmap和src_rect
5. 按照z轴顺序,渲染所有存在的Sprite到画面上
6. 给Viewport和Sprite定义了finalizer,使其在GC时也会释放相应的内存
7. 监听系统消息,点击右上角的 X 可以关闭窗口
8. 在exe中内置脚本 main.rb 和 imagesize.rb
9. 给exe添加了水印和简单的防篡改功能

后续的开发主要是在这个架构的基础上,增加绘制window/plane/tilemap的功能和一些bitmap的操作。当然,引入shader可能会有一些难度。

有兴趣的朋友可以看看src里的代码,脚本入口在script/entry.rb(因为main.rb已经内置不可修改)。

编译的exe文件和v0.2.1版本的代码: rgm_v0.2.1.zip (5.6 MB, 下载次数: 698076)
示意图:


作者: guoxiaomi    时间: 2022-5-8 01:29
本帖最后由 guoxiaomi 于 2022-5-8 01:43 编辑

Mark一下,居然已经写了5000行的代码~

下了个插件发现好像也没那么多,其中有1400行是RGSS内置数据结构,从F1里抄的。

只有2000行C++那我就放心了,预计能控制在4000行内。
作者: pillow鸽鸽    时间: 2022-5-11 00:00
佬!
你永远想象不到大佬背后的努力是多么
作者: guoxiaomi    时间: 2022-6-5 01:26
本帖最后由 guoxiaomi 于 2022-6-5 02:11 编辑

V0.4版本

度盘链接:https://pan.baidu.com/s/16XiBn3dfwEAEXc0XAGud3w?pwd=dgdi
提取码: dgdi

因为给 shader 传参数时调用了 D3DXGetShaderConstantTable,所以必须链接到 d3dx9_43.dll,已经附带在压缩包中。
main.exe 会执行内嵌的 main.rb 的内容,在加载 src/script 中各个脚本后,从 entry.rb 开始执行。
release.exe 执行的内容已经全部内嵌在 exe 中,还顺便打包了默认工程里的 Data 文件夹。

相比之前的v0.2版本,这次新增了很多功能:
1. 实现 color / tone / table 等基础数据类
2. 实现 RGSS 的几个可绘制类,但是目前只有 Sprite 能绘制
3. 实现 Sprite 绘制的全部功能,包括使用 shader 实现 tone
4. 实现 Input 的全部功能,允许绑定不同的按键,提供 Input.debug 供调试
5. 更新异步绘制流程,顺便提升了绘制效率
6. 实现 core::stopwatch,用于测试某一段重复运行的代码效率
7. 更新了 Makefile,并提供打包成 release 版本的功能
- release 版本的内嵌数据是加密的(但愿比默认加密强度高),包括一个 Data 文件夹和原来在 src/script 里的脚本

代码分布情况:

那个 ini 显然是测试工程里自带的 Game.ini 啦~

接下来的绘制相关的功能就没有什么难点了,对照着 RMXP 逐个实现就行。
再次感谢将 SDL2 和 HLSL 结合起来的项目:https://github.com/felipetavares/sdlrenderer-hlsl
作者: guoxiaomi    时间: 2022-7-10 01:00
V0.5.2版本

度盘链接:https://pan.baidu.com/s/1TGnar4v72MSPuMv7hD-doQ?pwd=e9ux
提取码: e9ux

主要把默认的directx9更换成了directx11,解决了大量的兼容性bug,目前Bitmap除去字体外的功能,Sprite和Viewport的全部功能都已经实现。

此外,加载程序也有一些区别,现在不再内置main.rb文件,而是直接读取load.rb并执行,release版本则会读取压缩包内的load.rb。

代码分布情况,其中scripts是ruby代码,shader可以忽略,core/base/rmxp是C++代码:

作者: guoxiaomi    时间: 2022-7-21 23:06
本帖最后由 guoxiaomi 于 2022-7-21 23:17 编辑

V0.6.1版本

度盘链接: https://pan.baidu.com/s/1GrMgYQh0b3pp-du4SKzQaQ?pwd=pxfv
提取码: pxfv

现在打包时会附上测试的工程,内含RTP。

实现了除了 Graphics.freeze 和 transition 以外全部的绘制功能。tilemap是工作的重点,实现了:
1. 支持修改z值(我想没人会需要这个)
2. 允许任意多的优先级和图层(虽然编辑器做不到,但是可以用脚本修改map_data和priorities)
3. 可以取消默认的周期性绘制(是不是今天才知道tilemap跟plane一样是平铺的)
4. 一些绘制上的优化处理(可把我累坏了)

优先级遮挡测试,保证跟RMXP一模一样~


代码量统计:

作者: wynn111    时间: 2022-7-22 00:04
大佬!小白默默膜拜、观望!!!
作者: guoxiaomi    时间: 2022-7-30 00:55
V0.7.0版本

度盘链接: https://pan.baidu.com/s/1ERxmrI2I9cz3EH2-o0zPXA?pwd=v2bu
提取码: v2bu

已经实现了RMXP的全部功能。音频部分使用SDL_Mixer完成,所以有一些功能限制:
1. 无法修改bgm和me的音调(pitch)
2. mid格式的循环标记无法识别,循环播放时会衔接不上。
3. mid格式的音乐作为bgm被me打断后,只能从头播放。
4. 001-System01.ogg,无法正常播放,可能因为太短了,实测转换成wav就可以。

此外添加了基于Fiddle实现的Win32API类,不过RGM是64位的程序,而原版RMXP是32位的,dll显然不兼容。这里只是提供了一种动态扩展的手段。

代码量统计:

作者: guoxiaomi    时间: 2022-8-16 22:57
V0.7.2版本

度盘链接: https://pan.baidu.com/s/1lHGJKm8uIqbrX9nbYU9LLQ?pwd=3bk3
提取码: 3bk3

1. 重写了music和sound的实现,修复了音频的一些bug,目前切换me还是偶有爆破音,有待进一步调整。
2. 非release版本现在可以读取命令行里的debug,btest和-v。
3. 添加了宏RGMLOAD,用来将ruby的VALUE转换成特定类型的变量。

接下来会添加输入法的功能,然后加上F1菜单。

但是接下来的更新速度就不能保证了,因为从明天开始我就要上班赚钱了XD,我的计划是10月完成alpha版本供测试,年底修复完反馈的bug发布正式版。

作者: kirh_036    时间: 2022-8-16 23:45
xp的好时代,来临力
作者: pporder    时间: 2022-8-22 01:45
太秀了,狠狠膜拜
作者: guoxiaomi    时间: 2022-9-4 19:28
本帖最后由 guoxiaomi 于 2022-9-5 12:29 编辑

最近写这个引擎烧掉了我大量的脑细胞,不过终于写完了!

给大家一个demo供测试吧: RGModern-Project1.zip (5.77 MB, 下载次数: 21)

与前面的版本不同,这个demo是32位的程序(ruby也是32位的),工程是默认的空工程,附带了001-system01.wav文件。

差不多就是这样了,等到10月份我会把项目整理好,开源到github上。

已知bug:
1. 战斗测试无渐变效果
作者: heipai    时间: 2022-9-13 20:38
本帖最后由 heipai 于 2022-9-15 08:03 编辑

可以直接为您开一个新版块,然后版块内全是大家用您的引擎做的游戏
想想就牛了,膜拜一波
可惜我64位
作者: guoxiaomi    时间: 2022-10-30 20:58
因为三次元的一些事情,发布推迟到12月。
作者: miantouchi    时间: 2023-2-19 20:11
这个RGM能跨平台打包安卓吗?
作者: kirh_036    时间: 2023-2-28 23:39
控帧改为等待信号实现

写了个demo,验证windows下的WaitableTimer和STL sleep_for,结果令人很意外很失望。

源码:
  1. #include <Windows.h>
  2. #include <stdio.h>
  3. #include <thread>
  4. #include "SDL.h"

  5. int main(int argc, char* argv[])
  6. {
  7.     uint64_t frequency = SDL_GetPerformanceFrequency();
  8. #if 1
  9.     HANDLE waitable_timer = CreateWaitableTimerW(
  10.         nullptr,
  11.         TRUE,
  12.         nullptr);
  13.     while (waitable_timer)
  14.     {
  15.         uint64_t before = SDL_GetPerformanceCounter();

  16.         LARGE_INTEGER delay;
  17.         delay.QuadPart = -1E7 / 60;
  18.         SetWaitableTimer(
  19.             waitable_timer,
  20.             &delay,
  21.             0, nullptr, nullptr, FALSE
  22.         );
  23.         WaitForSingleObject(waitable_timer, INFINITE);

  24.         uint64_t after = SDL_GetPerformanceCounter();
  25.         printf("call: %lld, real: %llu, error: %lld\n", -delay.QuadPart, after - before, llround(0.L + after - before + delay.QuadPart));
  26.     }
  27. #else
  28.     while (true)
  29.     {
  30.         uint64_t before = SDL_GetPerformanceCounter();

  31.         long long delay = 1E7 / 60;
  32.         std::this_thread::sleep_for(std::chrono::nanoseconds(delay * 100));

  33.         uint64_t after = SDL_GetPerformanceCounter();
  34.         printf("call: %lld, real: %llu, error: %lld\n", delay, after - before, llround(0.L + after - before - delay));
  35.     }
  36. #endif
  37.     return 0;
  38. }
复制代码


运行结果:
WaitableTimer



STL sleep_for



结论:
WaitableTimer的精度实际上和STL sleep_for一样,都是15ms左右(具体数字取决于操作系统的调度时间片)的粒度。

P.S.:github不好发图,就借楼发了
作者: guoxiaomi    时间: 2023-4-4 20:23
本帖最后由 guoxiaomi 于 2023-4-4 20:24 编辑

经过一段时间的开发,RGModern的1.0.0候选版本发布了。提供了范例工程,文档还在完善中……

点击下载 完整测试工程,或https下载

工程内部的Game.exe可以代替原版Game.exe使用,Gamew.exe则内嵌了加密的Data文件夹,用来测试数据加密的效果。

项目已开源,github仓库地址:https://github.com/gxm11/RGModern




欢迎光临 Project1 (https://rpg.blue/) Powered by Discuz! X3.1