铁血丹心

 找回密码
 我要成为铁血侠客
搜索
查看: 6029|回复: 8

[C++复刻] c++复刻版的画图和执行

[复制链接]
发表于 2017-10-29 11:58 | 显示全部楼层 |阅读模式

马上注册,结交更多侠友!

您需要 登录 才可以下载或查看,没有账号?我要成为铁血侠客

x
如果有需要的话,请上网找Visual Studio和c++的相关资料,基本的东西就不赘述了。

这里简单介绍一下c++版本中的一个特殊的地方,就是画图与执行与之前复刻版的区别。

之前复刻版的缺陷

在之前的版本中,画图和执行都是混在一起的。简述如下:
pascal版本中的基本结构是:大场景调用小场景,小场景调用战场。如果有菜单,则也是类似的被上一级调用的关系。
lua版中稍有区别,大场景和小场景在同一循环中处理,使用一个全局的变量作为标记,而战场,菜单与pascal版类似,是被调用的子程序。

从游戏的结构来看,大场景调用小场景,而非平级设计,应该是相对合理的,程序写起来也很自然。而lua版这样的设计也是有道理的,但是并没有贯彻到底,就是将战场也放到主循环,当然我们也可以认为之所以没有这样做,是因为需要返回一个值作为胜败的记录。更进一步,如果有必要的话,用户界面、菜单等也可以放到主循环,但是这样需要维护的状态量就实在太多了,特别是在出现多层菜单,或者执行事件的时候,可能会出现大量必须回调才能解决的问题,或者采用图灵机的模式,执行到哪里算哪里,导致编程难度指数上升。

基于这样相互调用的设计方案,两版都存在一个共同的问题,就是在某个部分(这里可以包含上菜单,对话框等)在执行的时候,其他部分是完全停止的。因为两个版本中,绘图、响应输入的代码基本是混杂在一起的。这其实在很多地方是一个很大的局限,在这个游戏中还不太明显,但是我们可以看一下jy027设计的三国志英杰传复刻版,在执行菜单的时候,人物的动作是不停止的,其实三国志英杰传的DOS版本也是如此。那么我们再回看金庸群侠传,就会发现这个方案的局限之处了。

让执行菜单的时候,同时更新所在场景的图像,也可以做到这点,但是这样的话,通常还有必要将场景刷新数据的函数一起放进来。这其实对程序的维护是不利的。大家可以看一下闭包的设计原则,就是一个模块应该只处理自己的部分,如果连其他的部分一起处理,后面的维护是很困难的。

上面说的可能比较抽象,这里举一个原有框架缺陷的例子。
现在大多数mod都是没有云这个设计的,如果你某一天高兴,想加几片云进去,这自然很好。于是你设计了两个函数,一个画云,一个改变云的坐标,然后把它们都放在大地图的函数里面。
那么当你进入小场景时,这两个函数应该都不运行了。于是你出小场景之后,云还是在原来的位置。这很奇怪,因为进去小场景后,可能过去了很久,发生了不少事,怎么云还在呢,时间停止流动了吗?
于是你就可能想把改变云坐标的部分加进小场景的函数里面。但是这也很奇怪,因为小场景里面根本就没画云,为什么要操作云的数据呢?而且,战斗场景、菜单等,是不是也要加呢?这里违反了闭包原则,一旦调用关系很深,可能需要在每个模块的执行函数中加上大量其他模块的函数,后期会很难维护。

设计简述

c++版的设计,采取的是另一种方式,就是画图和执行分离。这里存在一个全局的栈。在画图的时候,就画这个栈里面的东西,而运行的部分是独立的,只处理用户的输入如果影响自己部分的数据即可。这是基于同一时间,只准许一个模块接受用户输入这个假设,通常来说这个假设基本是合理的。

可能会有人觉得这跟上面提到的没什么不同,不还是需要在某个模块中处理其他模块的东西么?其实确实是这样的,但是这里通过接口与继承的方法避免了在程序上的直接处理,一个子模块只需处理好“如何画自己(draw)”和“如何响应输入(dealEvent)”这两个问题,具体执行由基类在相对底层的位置统一解决。这样在程序的编写中,仍然可以使用类似pascal版中相互调用的模式,响应事件的方式类似,而绘图方式则完全不同了。

而实际的代码中,则是多了一些细节,例如补充了一个模块在自己没在执行的时候应该做什么(backRun)等。
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
 楼主| 发表于 2017-10-29 21:58 | 显示全部楼层
状态栈

从这里开始,我们需要解释状态栈的概念。栈可以认为是一个有限制的数组,可以增删查改。但是栈的增删只能在栈尾进行。例如现有一个栈:
【1 2 3 4 5】(以“【”表示栈的头部,“】”表示栈的尾部)
那么你想删除5之前的元素,例如3,这是不允许的,你只能删除5。你在5之前插入一个值,这也是不允许的,只能放在5的后面。
例如,上面的栈执行删除得到:【1 2 3 4】
再执行添加元素6得到:【1 2 3 4 6】
这时如果想清空栈,需要连续执行5次删除。

现在我们回到游戏执行的问题。如果从主场景进入了子场景,当离开子场景的时候,之前的处理是将游戏状态再改回主场景。这里,我们需要在子场景函数的开始和结束加入修改状态的语句。
那么为什么要改回主场景,而不是改成战场或者标题呢?这是因为上一个场景是主场景。你可能觉得这是理所当然的事。但是如果某一次你真的是从战场进入了子场景呢?

如果使用状态栈描述,则游戏的执行如下:
游戏一开始:【标题】
进入主场景:【标题,主场景】
进入子场景:【标题,主场景,子场景】
离开子场景:【标题,主场景】
进入某菜单:【标题,主场景,菜单】
离开某菜单:【标题,主场景】
离开主场景:【标题】
这时我们可以发现,用状态栈来表示游戏的执行非常自然,只需将当前的状态增加到栈里面,离开时删除。那么任何时候检查栈的尾部,就知道哪一个元素正在执行。而不需要在开始和结束某个执行过程的时候特别更改状态。

绘图的时候,考虑菜单也是一个绘图元素,只绘制栈尾元素是不够的,应该从头到尾全部画出来才比较合理。但是考虑到有一些绘图元素是可以完全把下面的东西盖住的(例如主场景,子场景,战场等),可以增加一个判断,即某些元素存在的时候,可以不画其前面的元素,节省资源。

栈中所有的元素都不是死的,因此每个时间均应该检测栈中的所有元素,准许其改变所需的数据。例如主场景的云就是这样。只要没有退回标题,无论任何时候,都应该执行改变云坐标这个过程。

这样,主循环的设计理应如下:
  1. 检查栈中所有元素,改变数据;
  2. 依次检查栈中所有元素,记录最后一个占全屏的元素并从这里开始画直至栈尾;
  3. 执行栈尾元素的处理用户输入部分;
复制代码
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
 楼主| 发表于 2017-10-29 23:13 | 显示全部楼层
执行

那么依照上面的思路,开始思考下一个问题,如何执行剧情?

剧情中有几类指令:改变数据的,条件跳转的,需要绘图交互的(对话,菜单等)。

可以先思考一下,如何实现一个菜单?按照前面的想法,只需将菜单加入状态栈尾部,就会参与画图,并独占用户的输入。但是,当菜单执行完,被删除出栈的时候,前面的一层必须要知道,用户选了哪一项!

这样来看,菜单可能需要返回一个值,但是因为这种循环的机制,无法知道菜单在哪一个循环结束,也就不知道该如何获取这个返回值。

通常有两个方案:
1. 使用回调,即用户使用菜单的时候就命令其结束的时候应该执行一个指定的函数,可以立刻知道菜单的返回值。
2. 调用菜单的元素提供一个变量,让菜单可以修改它,通常来说是指针或者全局变量。而且在菜单的循环结束后,还需要检测这个值是否被改变。

无论哪种,都没有之前的用法:
  1. int ret = menu(...);
  2. if (ret==...) {...}
复制代码
来得方便。

下面继续思考剧情如何执行。剧情是分为很多指令,依次执行的,如果采取这种方式,剧情按理应当为一个执行元素,其绘图部分暂设为空。
执行元素其实就是一个循环,每个循环执行一条或者多条指令都是可以考虑的,如果遇到需要对话的情况,则可能需要加一个对话的执行和绘图元素。到这里其实还可以处理,但是遇到条件判断呢?

思考到现在,发现剧情脚本可能应该写成这样:

这个应该说是……图灵机……或者……编译器的语法树前端。这样,剧情反而不如用从前的一串数字来描述方便,脚本语言的好处完全没有用到。

到这里,我们发现了以下问题:

状态栈的设计可行,但是需要回调或者全局的状态量才能解决交互的问题。而剧情的脚本编写会比较麻烦,特别是使用条件判断时会出现更大的困难。

回到原来

这样我们发现了,状态栈能解决动态绘图的问题,但是程序不好写,状态量和回调太多。函数调用的方案写程序,做剧情时会简单很多,同时虽然也能解决动态绘图,但是违反闭包原则,需要在某个执行节点操作其他它节点的数据,代码有可能很难看,调用的情况如果很深则可能需要加上较多违反闭包原则的函数调用。

那么这时,我们可以利用接口继承的方式,将调用执行与状态栈结合起来。此时虽然仍然需要在每个执行节点中操作状态栈中其他节点的数据,但是在程序的形式上,二者却可以完全分离。

每个执行和绘图元素使用统一的接口来执行,形式如下
  1. 将自己加入状态栈
  2. 死循环 满足一定条件时退出
  3.   检查状态栈的元素,执行数据处理
  4.   检查状态栈的元素,执行绘图
  5.   检查用户输入并响应
  6. 将自己移出状态栈
复制代码


该函数仅存在于基类中,各自的子类则负责实现各自不同的的响应用户输入的方式,和绘制自己的方式。

这里则必须利用各自的程序设计语言提供的接口来实现,c++中是在基类定义虚函数,子类用同名函数覆盖来处理的。也就是说,同样的一个dealEvent函数,随着子类的不同,run部分会调用对应的同名但不同功能的函数。子类不得重新定义run函数,禁止直接操作状态栈,都是通过基类来操作。这样就可以实现画图和执行在子类中形式上完全分离,可以说是保留了状态栈容易处理绘图的优点,也保留了传统的模式中易于写程序的优点。

菜单或战斗的返回值,可以给run接口增加一个返回值来实现,也可以直接获取。

【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
 楼主| 发表于 2017-10-30 01:22 | 显示全部楼层
子节点

下面我们继续以菜单为例:如何画菜单,并且实现鼠标响应呢?

之前的做法都是画一个菜单,根据鼠标所在的位置,计算出鼠标位置对应的菜单项,并将其该项的画法稍微变动。
那么,如果我们想做一个横向的菜单呢?自然是计算方法稍微变化即可。
以上情况都对应的是菜单排列有规律,如果菜单的排列没有规律呢?那你可能回答,那就把这些项的位置记录下来啊。

这并不是解决问题的根本办法。实际上,判断哪个项被激活,是由菜单来控制的。也就是说,是菜单记录着每一项所在的位置,并加以判断。
这里我们换一个思路,让每一项记录自己的位置,并判断鼠标是否在自己所在的范围内。即子节点的设计。

一个菜单可以认为是一个节点,它包含了数个子节点,子节点记录了自己的位置,在画图时一起画出。而子节点同时负责判断鼠标是否在自己所在的范围内,并将被激活的节点告知菜单。在接收到点击事件时,只需判断哪个子节点处于激活。

这时你可能要问,这跟之前好像也没什么本质区别啊?但是这样的判断方式,是可以直接在基类中处理的,这就简化了子类的程序编写。

子节点随根节点一同画出,和子节点记录自己的位置并判断是否被激活,是这个框架中的两个重要设计。
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
发表于 2017-11-21 21:25 | 显示全部楼层
能不能写个源码导读。。。
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
发表于 2017-11-29 03:08 | 显示全部楼层
c++复刻版的画图和执行
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
发表于 2017-11-29 14:37 | 显示全部楼层
谢谢分享 多谢了  虽然我的知识能力真心有限 我已经看不懂了  谢谢
自zzdyw.cn 评
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
发表于 2020-12-13 21:10 | 显示全部楼层
很多设计模式的知识啊,面向对象要用的好,设计模式是一定要学的~
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
发表于 2022-4-4 15:15 | 显示全部楼层
好深奥啊,大大给力
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。

本版积分规则

小黑屋|手机版|铁血丹心

GMT+8, 2024-11-21 20:42

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表