查看: 2066|回复: 6

[讨论] 【杂谈】游戏开发中的数值变量技法

[复制链接]

36

主题

717

回帖

13

精华

版主

经验
7323
硬币
1151 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

发表于 2022-8-4 14:22:18 | 显示全部楼层 |阅读模式
游戏开发,无非是把我们所希望设计的「行为」转换为「逻辑」,在这个过程中,变量可以说是发挥着关键的作用。不过关于变量在游戏开发中怎么去用,似乎没有什么理论参考,于是我打算在本帖将我的经验作一个简单总结。考虑到社区内目前最常用的还是 Clickteam Fusion,本文主要谈一谈关于数值变量(换句话说,int 型或者 double 型变量?)在游戏开发中的相关技法。

评分

参与人数 1经验 +2 收起 理由
1168438795 + 2

查看全部评分

Moonstruck Blossom
个人网站:dasasdhba.github.io

87

主题

1713

回帖

11

精华

超级版主

经验
10236
硬币
1242 枚

赞助用户永吧十五周年倒计时海报勋章第三届MW杯冠军第十一届MW杯四强PK!MF3 冠军PK!MF5 季军PK!MF4 殿军欢乐演员人气之王

发表于 2022-8-4 14:23:12 | 显示全部楼层
这个很不错,适合代码入门
自己制作的游戏The Frontiers 点击进入

36

主题

717

回帖

13

精华

版主

经验
7323
硬币
1151 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2022-8-4 14:46:43 | 显示全部楼层
一、常用小功能
为了作一些铺垫,先介绍一些数值变量的常用功能吧。
1. 计时与计数
整数数值变量可以用于计时,如下图:

不过这样做的话,单位就是帧了,MF 的默认帧数是 50,也就是 50 帧为一秒啦。
值得一提的是 CTF 自带的 Timer 是帧率无关的,这也是造成卡机高跳的原因之一,所以修复方式就是改用此种方式计时。
计数也是几乎同理,把 always 改为某种条件,就能在需要的情况进行计数了。

2. 暂时存储特定信息
很多时候我们暂时需要存储一些信息。
比如石盾,我们需要存储它的初始 Y 坐标用于判定什么时候返回;
比如探照灯,我们需要存储它的中心坐标以使用参数方程使其旋转;
那么数值变量很自然可以做这件事情了。

此外,还有一种特殊情况是随机数,我们有时候可能需要一个随机的过程。
此时可以用一个变量来存储这个随机值:变量 R = Random(3)。

3. 追加「属性」
举个例子,CTF 中每个 Active 都有一个坐标属性(Positioin),我们可以像变量一样去设置它,将它与特定数值进行比较。
换句话说,物件的 XY 坐标跟变量没什么区别(不过 CTF 的坐标是 int 型,需要注意),只是它被赋予了坐标的属性,程序会根据这个值去设置其位置。
但我们也可以人为地赋予一些变量以属性,比如下图中,我赋予了变量 V 横向速度的属性(为什么?):

赋予变量以属性一般可以带来两个好处:
1. 接下来你就可以像操作物件的坐标一样来操作物件的横向速度了;
2. 你可以将这个属性参数化,并方便地进行调整,比如 RE 的组 1 就赋予了变量 A 以横向速度的属性。
注:对于 CTF 而言,还有第三个好处:避免指代问题。
具体来说,当你用变量去作比较的时候,是有筛选效果的,但直接比较一些属性的时候却没有筛选效果,比如,下图没有筛选效果:

angle 与坐标类似,是物件的一个属性,但由于 CTF 限制,直接比较没有筛选效果,而如果我们赋予变量 W 以 angle 的属性,这个问题就解决了:


那么铺垫工作就结束了,接下来我们将会谈到一个比较核心的内容。
Moonstruck Blossom
个人网站:dasasdhba.github.io

36

主题

717

回帖

13

精华

版主

经验
7323
硬币
1151 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2022-8-4 15:33:27 | 显示全部楼层
二、核心思想:利用数值变量「阶段化」行为
这一部分也可以算是数值变量的一个功能,只是我个人认为这个功能极为核心,所以就单独谈了。
先解释一下「阶段」:当我们需要实现一个复杂的行为时,这个行为往往可以分为若干个小的阶段,因此我们只需要将这些小的阶段各个击破即可。
(有点类似于将一个大目标分解为若干个小目标之类的说法。)

我们以食人花为例,食人花的行为可以简单分为这样的阶段:
  • 在水管内,并检测是否可以出水管;
  • 出水管;
  • 在水管外,能喷子弹就先喷子弹,然后计时以决定什么时候钻回去;
  • 进水管。

依次循环,便完成了食人花的行为。

接下来谈谈具体实现,为了将这些阶段分解,我们便需要利用数值变量「阶段化」。
在上述食人花的例子中,我可以定义:变量 Z=0 为 1 阶段,Z=1 为 2 阶段,Z=2 为 3 阶段,Z=3 为 4 阶段。
接下来我们只需要分别完成这四个阶段的行为就可以了。

不过需要注意的是,每一次转阶段的时候,你都要保证初始状态是你所预设的。
举个例子,如果你在即将要转入的阶段使用了变量 T 来计时,那么你需要保证转入这个阶段的时候,变量 T 被提前设置为了 0。

对于阶段一,我们只需要简单比较马里奥是否在害羞范围内,如果在就利用刚才提到的办法计时,达到设定时间后设置 Z = 1 进入下一阶段即可;
对于阶段二,简单地操作一下坐标就可以了,出水管结束之后进入下一阶段即可;
对于阶段三,我们又可以分为两个小阶段:
  • 喷子弹,如果喷完了/不喷子弹,则进入下一小阶段;
  • 计时,达到设定时间后设置 Z = 2 进入下一大阶段即可;

而对于「喷子弹」过程,我们又可以分为两个小阶段来实现:
  • 创建一个子弹,设置其坐标和速度等参数,并利用刚才提到的计数变量计数,随后立刻进入下一小阶段;
  • 计时,达到设定时间后判定刚才计数的数量有没有达到设定值,如果没有,返回第一小阶段进行循环,反之喷子弹过程结束。


最后我们到了阶段四,这个过程跟阶段二完全类似,归位之后回到阶段一即可。
不过你可能会发现我们也许需要暂时存储一下食人花的初始坐标,不然归位可能不太好办。

如果你理解了上述过程,不妨自己试试动手写一个食人花,看看是否顺利。
值得一提的是,RE 的食人花没有使用「阶段化」方法,因此其逻辑看起来十分混乱难懂,不过毕竟作者是在没有学过编程的情况下做出来的,也不必太过计较啦。

再举一个例子吧:石盾——同样是 RE 中没有使用「阶段化」方法的一个物件,让我们试试用这种方法来看,石盾实现有多简单:

  • 悬空,判定马里奥是否在触发范围内,如果在的话,进入下一阶段;
  • 下落,落地的时候播放音效,创建爆炸特效,并进入下一阶段;
  • 计时,达到设定值进入下一阶段;
  • 归位,回到初始位置或者中途遇到障碍的时候,回到第一阶段。


真巧,也是四个阶段,而且看上去比食人花要简单不少。

那我们来看一个稍微麻烦点的吧——库巴:
首先,我们注意到库巴的运动和其攻击有时候是并行的,有时候又是分开的,为了方便,我们可以将库巴运动用一个开关(布尔变量)控制。
接下来我们来处理库巴运动,其可以分为移动和跳跃。

先看移动吧:
首先移动有一个方向,我们可以用一个变量来代表方向,-1 为左,1 为右。
为了达到转向的目的,我们只需要在库巴运动到转向边界的时候,设置一下这个方向变量就好。
然后是恶劣的「急停」,可以分为三个阶段:
  • 计时,达到特定值进入下一阶段;
  • 利用刚才提到的方法将某个变量设为随机数,在某个范围内时进入下一阶段,否则回到第一阶段;
  • 急停并计时(此时可以关闭刚才提到的库巴运动开关以达到效果),达到特定值后返回第一阶段。


接下来是跳跃,其实完全可以照搬急停的模板:
  • 计时,达到特定值进入下一阶段;
  • 利用刚才提到的方法将某个变量设为随机数,在某个范围内时进入下一阶段,否则回到第一阶段;
  • 跳跃(初速度也可在转阶段时设为随机数),落地后回到第一阶段。


那么运动就解决了,接下来我们看看库巴的攻击(8-4),我们可以将每一个招式看作一个大阶段:
  • 喷几个火;
  • 喷三重火;
  • 丢一堆锤子;
  • 喷几个火;
  • 喷一堆火;


这里有重复的阶段(阶段一和阶段四),我们在用变量划分阶段的时候用 OR 处理一下就可以了(阶段变量 = 0 或 阶段变量 = 3)。
接下来我们依次看。

喷火:
  • 计时,达到特定值后进入喷火动画,并随即进入阶段二;
  • 喷火动画结束后,创建火焰,播放音效,计数,并随即进入阶段三;
  • 判断计数是否达到设定值,如果没有,返回阶段一;反之,喷火阶段结束,进入其他招式。


喷三重火与喷火类似,只是不需要我们计数了,略去。

丢锤子:
  • 关闭库巴运动开关,切换动画并计时,达到特定值后切换动画并进入阶段二;
  • 创建锤子,计数 A(扔锤子数),随即进入阶段三;
  • 计时,达到特定值后,如果计数 A 达到特定值则进入阶段四,否则返回阶段二;
  • 计数 B(波数),如果计数 B 达到特定值则丢锤子阶段结束,重启库巴运动开关,否则进入阶段五;
  • 计时,达到特定值后返回阶段二。


狂喷火球:
  • 关闭库巴运动开关,切换动画并计时,达到特定值后切换动画并进入阶段二;
  • 创建火球,播放音效,计数,随即进入阶段三;
  • 计时,达到特定值后,如果计数达到特定值则狂喷火球阶段结束,重启库巴运动开关,否则返回阶段二。


那么一个 8-4 的库巴的全部内容,也就是上面这些了,其实每一个阶段都不难实现,但合起来看上去就比较复杂了。
综上,我认为使用数值变量「阶段化」我们所需要设计的行为,是帮助我们清晰地、有序地去实现逻辑的一个重要手段。
Moonstruck Blossom
个人网站:dasasdhba.github.io

36

主题

717

回帖

13

精华

版主

经验
7323
硬币
1151 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2022-8-4 15:44:34 | 显示全部楼层
本帖最后由 dasasdhba 于 2022-8-4 15:48 编辑

三、补充内容
最后再补充一些相对不那么重要的边角内容吧。

1. 利用二进制将数值变量作为布尔变量(开关)数组
这个方法好像有个术语叫「状压」,通俗地说就是将整数看作二进制,其每一位要么是 0 要么是 1,那么每一位都可以看成一个布尔值(开关)。
而通过二进制运算(与 &,或 |,异或 ^)我们可以很容易判别哪些位是 1,哪些位是 0,这样我们就可以把一个整数变量当开关数组用了。
值得一提的是 CTF 压根没有数组,所以这种方法还是有点实用价值的。

2. 绕过 CTF 指代问题
这个就纯属 CTF 专属的邪门技巧了(
我之前在问答帖讲过,以下写法中,不等式右侧是不具有筛选作用的:

                               
登录/注册后可看大图

具体体现在不等式右侧的变量 B 一定属于某个特定的 Active 实例而非变量 A 所属的 Active。
但我们可以通过一种邪门的方式绕过这个问题:

呃……我不是很想评价这种做法,不过这种邪门技巧也只有在 CTF 下才有用了((
Moonstruck Blossom
个人网站:dasasdhba.github.io

36

主题

717

回帖

13

精华

版主

经验
7323
硬币
1151 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2022-8-4 15:45:52 | 显示全部楼层
那么以上就是本帖的全部内容了,希望能对各位有些帮助吧。
如果你还有什么好的想法,欢迎提出和讨论。
Moonstruck Blossom
个人网站:dasasdhba.github.io

57

主题

425

回帖

8

精华

版主

☯ 博 丽 不 是 灵 梦 ☯

经验
6138
硬币
1021 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

发表于 2022-8-5 18:54:14 来自手机 | 显示全部楼层
先补充一下第二个大标题的东西吧:
所谓的【阶段】,专业上叫状态机,顾名思义,就是表示状态的【机器】,而这里的机器就是条件。
然后就是我刚刚在想一个问题:能否用一个变量做出数组的效果,
就是,多个数数通过一定的函数关系加和进一个数值里,读取的时候按照一定的函数关系再逐一逆向读出。
不过唯一一个缺点就是不能超过int或者float的数值上下限
目前的缺陷就是:逆向读出的时候不能确定原来的函数关系
>❀ To the Best You ❀<
您需要登录后才可以回帖 登录 | 创建账户

本版积分规则