大概写出来也会是很乱的东西……于是就直接按照想法来写了。可能并没有比较好看的结构什么的。
觉得这个会成为一个策划向的东西,所以程序可能会觉得太容易什么的,不过以前没接触过游戏方面的或许也可以稍微看看0 0
只是笔记的性质吧,同时可能也适合初次接触游戏的孩纸看一下,因为所见过的这方面的参考资料不是很多因此大概会有一些错误或者不太确认的地方……
————————–
在开始写之前突然想到Video Game和其他一些东西不同的地方。
其中一个就是有些书上说到的(Windows里面)GetMessage/PeekMessage这两个东西。前面那个是一直等着直到发现玩家点了窗口右上角那个叉或者其他的一些消息。后面那个就是不断地去瞥一眼有没有消息了,如果有的话就处理掉,然后继续不断地看……所以在游戏的程序里面,可能比较少地会看到“拉动一个开关然后触发机关”(但是玩家反而可以在解谜游戏里面经常看到:)),但是比较多地看到不断检查一个开关的状态,如果发现它被拉动了,它后面的机关就被触发起来。
嗯,比如按键。比如在角色游戏中,可以这样表示,如果玩家按了上就告诉主角,然后让他往上走。还有一种方式是让主角去检查玩家的按键情况,如果发现玩家按了上了,就往上走。很多时候,情况会比这个例子复杂一些,比如当玩家同时按下了上和右,主角会不会往右上方走呢?如果分成上和右两个指令,主角还是需要在大脑里记住之前收到的指令,然后合成一个往右上方走的最终效果。这样和上面的第二种方式就很类似了,因为同样是依靠状态,如果是上面第二种方式的做法,那就是主角去检查玩家是不是按了上并且按了右。看上去第一种方法比第二种要麻烦,但是之前经过别人的提醒想到一个解决方案,那就是把右上变成一个单独的指令,这样主角虽然接到了右并且接到了上,但是更重要的是主角接到一个右上的指令,然后主角就可以直接往右上方走了。
除了上面的,还有更加更加复杂的情况。比如格斗游戏里面有各种复杂的组合按键(几乎没玩过这类游戏……一个原因就是怕复杂的组合按键和对敏捷反应的要求吧,另外也是自己不感兴趣……),这种时候第一种方式可能会有各种复杂的指令,最后可以排成一个巨大的指令表,第二种方式可能就会有大量的一层一层的状态判断。嘛,总之里面的按键组合本身就很复杂,不管如何都简化不了吧。至于最后要用第一种还是第二种,可能也有一个折中(折中这个东西真的是到处都有啊……)。另外
同时收到多个指令的情况下,也有优先顺序的判断之类,比如主角已经往右上走了,就不需要再往右走了。这里还是逃不了状态吧。
所以一直觉得,Video Game很多时候都是各种状态变换所组合起来的。写到这里,突然觉得游戏和平时使用的窗体程序区别还是挺大的,反而更接近视频之类的东西。视频和游戏同样具有fps的概念,同样是根据帧来刷新的,同样是一个连续的过程,而且video game的video刚好也可以指视频……(虽说貌似video原意是来指video device的)。于是这样看起来,游戏不论是设计或者是写程序框架的时候,看作那种等待使用者给一个指令,然后做相应事情的似乎不是很合适了。更合适的是让游戏一直Loop着,然后顺便看一眼玩家有没有给什么指令,然后处理掉,并且继续进行新一轮的Loop……可能因为游戏里面的这个循环,所以很多时候状态就会看上去比用事件、命令更加方便~
补充:后来了解到关于事件处理按键的一些更详细实例,其实处理的方法往往是事件和状态的结合。所以说用哪一种说到底可能还是习惯问题吧,我觉得状态在这个场合中比事件更直接,减小一些麻烦,因此觉得完全的状态会更好一些。
————————–
一般来说游戏里的循环是这样的形态
1 2 3 4 5 6 | loop do message_handle game_update game_draw wait_for_next_loop end |
message_handle就是上面说到的如果玩家点了叉,或者其他的信息,就在这里面处理。game_update是进行游戏里的数据更新,比如主角得到宝物攻击力上升、敌人的智能考虑要采取什么行动才能攻击主角、下雨效果中雨点下落的位置变化。game_draw就是把各种内容绘制到屏幕上了,比如主角的图形、敌人攻击的动画光效、下雨效果中的所有雨点等等。wait_for_next_loop是做什么呢?比如很多游戏经常用60fps,这样的话游戏循环一次(包括刷新绘制等等的)就是16毫秒。但是有可能上面的做完以后只花费了1毫秒,这样就需要继续等15毫秒,直到所有的事情做完。
关于这个循环有很多的变形。比如
1 2 3 4 5 6 | loop do message_handle game_update game_draw every two loop wait_for_next_loop end |
有可能绘制特别耗时间,那么就减少绘制的次数吧。刷新2次才绘制1次,这样绘制的fps其实只有30,32毫秒才绘制一次,但是游戏的刷新依然是60。游戏的运行数据完全没变,但是绘制的次数适当减少了,视觉效果也没有受到太大的影响(因为是大于一般人眼的暂留上限嘛)。
还有其他的例子。
1 2 3 4 5 6 7 8 9 10 | loop 1 message_handle game_update wait_for_next_loop end loop 2 game_draw wait_for_next_loop end start_loops(loop1, loop2) |
这样的场合,update和draw的fps就不会互相影响了。就算draw非常慢,update如果本身并不慢,就可以维持自己的fps。但是,万一两个循环分别运行的过程中,update和draw突然用到了同一个东西,这种时候就会麻烦一些。
因此还有下面的方案
1 2 3 4 5 6 7 8 9 | loop do message_handle count = @draw_last * updates_per_second count.times do game_update end game_draw_and_wait @draw_last = get_draw_last end |
这里draw和wait放在一起了,而@draw_last获取的就是draw和wait加起来所花去的时间。当draw时间太长的时候,就不需要wait,直接执行下一次循环了吧。然后计算在这些时间里面需要update几次。如果draw花去的时间不多,那么draw和wait的时间是正常的,在60fps下面就应该是1/60s。第二次循环的时候,计算得到的count就是这个时间乘以一秒钟刷新的次数,于是得到刷新一次。但是如果draw花去了循环2次的时间,那么得到的时间就是1/30s了。第二次循环的时候把它乘60,得到两次,于是这个时候就瞬间刷新两次。这种情况下,第一次循环结束到第二次循环刷新完成,update瞬间做了两次(当update运行相比draw来说很快的时候就可以看成瞬间了……),draw也同样花费了两次刷新的时间。看上去和上面两个循环分开处理的是同样的效果,在使用的时候也可以当成两个循环分开的那种效果来用了。在时间上还可以用一个更准确的计数方式,用于出现draw刷新1.5次之类不是整数次的时候,update的次数更准确。游戏框架XNA用的就是这种方式了。
RPG Maker的循环,利用了一个写作Graphics.update的东西。这个里面同时包括了绘制和等待。但是默认代码中使用它整体的结构是
1 2 3 4 5 | loop do Graphics.update #这里省略一些关于Graphics的内容 scene.update end |
scene.update就是上面的game_update了。这样其实和第一种是类似的。当绘制慢的时候,update就也被拖慢了,于是出现游戏整体慢下来的情况。而XNA的那种形式,绘制慢了而update不变,就会出现画面一跳一跳的样式。这两种效果其实都是绘制太慢时候的“异常状况”吧。虽然尽量避免是最主要的,但是也要考虑万一出现了,选用哪一种情况会更好一些,比如一些需要严格进展、不能被拖慢进度的场合,就宁愿要选用画面可能跳动的那种了嗯。
————————–
前面所说到的这个内容,看上去和程序的关系比较大,但是从关系到游戏体验的角度来看,却好像和策划的关系也挺大的。这个大概就是一种边界问题了吧。
唔,好像上面的和主题的关系稍微有些远了。回望一下标题,突然发现有2d,于是想当时为什么写了个这样的标题呢?或许想象中的2d游戏都是比较小规模的,觉得拿比较小一些的游戏会稍微好写一点吧。不过说来也是,对于游戏程序来说,3d游戏在绘图方面会比2d游戏麻烦很多,于是2d游戏的代码大部分都可以集中在游戏流程等等一些绘图之外的功能上了。
比如一个非常非常传统简单的俄罗斯方块,在写程序的时候首先先要找一个地方储存游戏的状态,也就是游戏盘里面已经堆了哪些砖块,堆在什么位置。游戏的update可以很慢,比如1秒update三四次,在update的时候让还在空中的砖块往下降落一格,如果按了方向键,就进行平移旋转等等(因为是非常传统那种,于是不考虑方块的平滑下落和平滑旋转之类了)。然后draw做的事情就是把游戏盘的图样画出来给玩家看。画出来的过程也很容易,只需要从上到下分别判断每一个格子处于哪个砖块,分别是什么颜色,然后给格子填上对应的颜色就好了,如果找下来发现这一个格子是空的,涂上背景颜色就可以完成。
然而当游戏的复杂性越来越高,游戏里面要显示出来的东西越来越多,update和draw就会同时变得越来越复杂。update里面会看到各种对象的刷新,而且draw里面也会看到纷繁复杂的绘制。如果要添加一个新的东西,需要在很多地方添加上新的代码。当然因为这个东西是新的嘛,所以肯定会有新的代码加进去,但是能不能尽量让它显得简单一些呢?
————————–
在RPG Maker的脚本中(游戏的逻辑就是在脚本里面处理的),只有各种update,并没有看到那么多的draw。就一个Graphics.update代替了各种复杂的draw,它是如何做到的呢?
准确的说,Graphics.update是代替了一部分draw吧。RPG Maker中有一个叫sprite(“精灵”?雪碧?)的东西,这也是2d游戏里面的一个通用称呼。处理2d游戏很多时候并不会在每一帧去查看游戏中分别有哪些东西需要绘制,然后分别找一些相应的位图数据绘制到屏幕上;而是经常会用一种sprite的东西表示画面上的一张图片,在绘制的时候根据Sprite来决定要绘制哪些图片,绘制在什么位置,需不需要带透明、缩放、旋转等等。但是这样看起来,复杂程度不是还没有改变吗?只是从draw转移到了sprite上面而已。虽然如此,sprite生成以后就不需要去纠结它每时每刻的draw了,因为sprite的结构都长得一样,只需要让系统自动去处理sprite的绘制而不是手写各种不同东西的绘制就可以了,这里还是可以省下一些工作量的,而且程序的结构也可以更加清晰一些。
使用RPG Maker的时候,甚至都不需要考虑sprite被绘制之类的详细东西。使用的人看到出现sprite,就可以认为是画面上添加了一个图片;修改sprite的坐标,就可以认为是画面上的图片移动了一下位置,而不需要认为是每一帧换了个位置重新画之类。有人推测它的原理,在新生成一个精灵(Sprite.new)的时候,它已经在全局的某处“注册”了一下,然后Graphics只要在轮到绘制的时候,去注册的地方找一找有多少精灵,然后把它们排个序,再根据精灵找了相应的图片,按照相应的参数画到屏幕上就好了。
顺便的,RPG Maker的精灵排序也很仔细。首先当然是和一般的处理方式一样,根据一个表示深度的变量z来对精灵排序,虽然是2d游戏,但是假想有一个垂直于屏幕方向的z轴,根据z的大小可以确定sprite的先后层次,就如同叠在桌上的不同层次的书那样。如果遇到z相等,它还会按照y进行排序。这个细节在绘制地图图块和事件的时候会方便一些。比如地图下面有一棵高高的树,它就会挡住上面一定范围内的图块和事件,而树根会被下面经过的有一定高度的行人挡住。就算所有这些的y都一样,只需要把这些精灵的原点设置在脚下,根据它们脚下的高低,就能判断覆盖上的关系。人物上下左右移动时,也不需要时时刻刻修改z的值了,层次的先后自动根据y的变化而变化。
在Flash中,可以看到另外一种处理sprite的样式。sprite还有一些其他东西,都有下面的看上去很长的继承关系:
Sprite → DisplayObjectContainer → InteractiveObject → DisplayObject → ……
从右边开始看,DisplayObject负责的是把某个东西显示在屏幕上(事实上DisplayObject并不带把物体显示在屏幕上的API,它提供的是类似x y坐标之类的显示用的数据,但是一些继承它的东西都是可以显示在屏幕上的)。InteractiveObject负责的是交互,比如鼠标点击之类。DisplayObjectContainer大概是最神奇的东西,它是一个“容器”,可以装各种DisplayObject当然也可以装同样的容器。所以到最后,形成的就是一层层嵌套的那个结构。和RPG Maker的不同,Flash里面并不是把所有要显示的东西都堆在一起,而是从最上面的一个Stage开始,逐渐往下检查容器里有哪些东西,包括容器本身,最后同样是把所有的对象按照一定的顺序绘制到了屏幕上。相比RPG Maker,这里要多一个AddChild,也就是把新创建的东西添加到某个指定的容器里面,而不是在生成的时候自动集中到了一个统一的大容器里。
————————–
看flash的结构,许多flash小游戏都是用帧来处理一些东西的,比如在第一帧放几个按钮作为游戏开始界面,第二帧画一个地图之类;或者一个场景命名为标题,另一个场景命名为地图之类。因为flash的时间轴最直接的用处就是用来制作动画,而放到游戏中来看,时间轴可以用来表示游戏的“进程”吧,比如时间轴的每一帧作为迷宫游戏里的一个迷宫,当冒险者到达终点的时候时间轴跳到下一帧,也就是进入了下一个迷宫,同时也表示游戏的进程推进了。也有的游戏打开菜单可以进入某一个特定帧,这也可以看做是进程中的一个分支吧。
在其他的,没有时间轴的系统里面,用同样的方法来做是否可行呢?因为没有可以供编辑的时间轴,所以可以在代码里面设置一个叫做进度的变量。当进程推进或者跳跃的时候,可以给这个变量进行增减等等的处理。然后在update中,可以根据进程这一个变量目前的值,来控制画面中的各种精灵是否显示之类。比如标题画面等待玩家按键的时候,进度的值不变;而玩家按下开始按键后,(某一个“游戏开始”的开关被打开,)进度的值就可以在update开始增加了。同时,所有画面上的内容可以淡出,这里就举例子20帧吧。假设标题的进度数值固定是1,按下确定键以后数值从1开始增加,那么当数值达到21的时候,之前标题上的内容已经完全消失了,可以往标题上淡入新的东西比如地图之类。于是用同样的方法,举例子就用20帧淡入地图上的东西,这样41帧的时候,正式进入了地图界面。接下来的进程变化就是根据地图来变化的了。
下面一段简单的update代码大概可以说明上面所说的过程……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | def update # 如果开始按钮被按下了,那么就把一个游戏开始了的标记设置成真的。 @game_started = true if @start_button.pressed? # 下面是讨论游戏进度不同的情况下如何刷新 case @game_process when 1 # 当进度还等于1,表明还在标题,那么如果游戏开始了,就把进度往后推一格,来让下一次循环的时候进度能进入2开始的那个分支(然后就不断增加了)。 if @game_started @game_process += 1 end when 2 .. 21 # 当进度在2~21的时候,不断把进度+1,并且让标题背景画面和开始游戏的按钮不断变得透明。 @game_process += 1 @sprite_title_back.alpha -= 0.05 @start_button.sprite.alpha -= 0.05 when 22 .. 41 # 当进度在22~41的时候,同样把进度+1,并且让之前隐藏着的地图、主角等由透明显现出来。 # 地图、主角在这个时候一开始是完全透明看不到的。 @game_process += 1 @sprite_map.alpha += 0.05 @sprite_actor.alpha += 0.05 when 42 # 这里是有关地图的刷新了。 end end |
第一次看到这样的东西可能会对+1感到有些费解。于是想到一个更简单的例子:在游戏循环的结构当中,给定一个精灵,它的坐标是(0, 0)也就是原点了,如何把它的坐标以一定的速度逐渐移动到(100, 0)这样的位置呢?
可能刚接触的时候会在update里面这样写
1 2 3 4 5 6 7 8 | def update loop do # @sprite表示某一个精灵,@velocity表示速度 @sprite.x += @velocity # 如果精灵坐标到达了指定的位置,就跳出循环,让它不再继续运动了。 break if @sprite.x >= 100 end end |
但是这样的测试结果却是精灵在瞬间移动到了那个位置。
因为上面的循环是在一次update中处理的,也就是说这么多次坐标增加其实只是在1帧内完成了。要想让人看上去它是慢慢增加的,就需要在每一次update中分别增加一小段距离,就像上面的+1那样,每一次update只给进度+1,这样进度同时就能表示从某一个时刻开始经过的帧数了。就像这样~
1 2 3 | def update @sprite.x += @velocity if @sprite.x < 100 end |
————————–
时间轴大概是一个比较简单的把精灵“串连”起来的方式吧。除此之外flash里面还有一个叫做场景的东西,有一些游戏也用“场景”和时间轴一起来管理精灵之类的东西。在其他的地方写游戏,也会用场景这个东西来让结构变得进一步好看。可能程序会觉得,用这么复杂的结构还不如从最简单的开始写,还显得更直接;策划却会觉得,程序上这一层层包装上去的方式太麻烦了,从零开始写程序还不如用编辑器来编辑。
嗯,首先,如果游戏的规模大了,不得不分成一层一层的结构,每一层的规模都小一些。话说把一个大的程序分得小还有别的方式,比如根据功能来分之类,就像很多策划案里写的XX系统XX系统那样(虽说自己并不是很喜欢XX系统这样的说法……),于是这里就给一个称呼吧,从底层绘图之类的到顶层游戏逻辑的算是“纵向”的分块,根据功能来分的叫做“横向”的分块,那么一个游戏足够大的时候,光靠纵向或者横向都没法分得足够小,只有纵向横向一起用了效果才会更好。
然后关于编辑器,编译器能够看到的是游戏“纵向”的最上面一块,能够几乎完全不用关注原理,集中精神来考虑游戏逻辑。但是游戏类型各有不同,市面上的商业编辑器不一定能符合每一个游戏的需求,所以很多团队的程序在制作的过程中开发着自己用的编辑器。比特殊类型的游戏编辑器(比如RPG Maker和Action Game Maker等等)稍微低一些层次的是游戏引擎里面的编辑器,它们通用性比较好,所以也要求使用者除了设计之外还懂一些编码,或者同时有懂编码、懂设计的多个使用者来用吧。总之当现有的编辑器没法满足要求的时候,就只能麻烦程序来开发了……
于是回到关于场景的话题。说“场景”其实是把时间轴组织得复杂一些,并且带上了相应的精灵的管理。比如游戏的标题、地图、游戏结束界面分别可以看成不同的场景,游戏里面不同的地图场所也可以看成不同的场景。更具体来说,比如有一个场景叫做标题,标题场景存储着标题里用到的一些精灵和按钮等等,并且同样带有一个叫做update的东西(按钮就看成里面带着精灵的一个特殊物体吧,可以通过update来刷新当前有没有被按下的状态)。场景的结构和用法可能会像下面的代码这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class TitleScene # 初始化过程 def initialize # 生成各种需要用到的物件 @back_sprite = Sprite.new(...) @button_start = Button.new(...) @button_exit = Button.new(...) end # 刷新场景,具体来说是刷新按钮的状态、判断按钮有没有被按下。 def update @button_start.update @button_exit.update start_game if @button_start.pressed? exit_game if @button_exit.pressed? end def start_game # 这里写上开始游戏的处理 end def exit_game # 这里写上退出游戏的处理 end end |
如果要做淡入淡出之类的效果,上面说的时间轴依然是需要的,然而这样写了以后就不需要整个游戏用一个大的时间轴了,把大的时间轴根据功能的不同拆到几个场景里,看着也会更加赏心悦目~
关于场景的刷新,可以用这样一种很直接的方式。
1 2 3 4 5 6 7 8 9 10 11 12 | class Game def initialize $scene = TitleScene.new run end # 游戏的主循环放在这里 def run loop do $scene.update end end end |
但是这样忽略了一个东西:场景的切换。因为$scene是一个全局的东西,于是把它修改掉了就相当于场景切换了,但是老的场景里的那些东西还在,需要去掉它们。于是变成这样:
1 2 3 4 5 6 7 8 9 10 11 12 | class TitleScene def update ...... # 上面是之前update的各种东西,下面则是处理场景变化的场合了。 # 如果全局里的那个场景已经不是本身,就是说场景已经切换了,下一帧要执行的update就不应该是这个update,而是新场景的update了。这时候需要做场景消失时候的工作。 terminate if $scene != self end # 场景结束时的处理 def terminate ... end end |
看上去应该是旧的场景完全被丢弃后,新的场景才能被添加上去。但是这个时候先把新的场景替换了旧的场景的位置,然后再让旧场景去做收尾工作,等下一次刷新就轮到新的场景进行刷新了。想上去似乎不是很通顺,但是这样确实可以让整个东西变得非常简单,否则旧场景消失到新场景添加的一段时间,会有些令人纠结。
————————–
上面提到的场景和传说中场景管理似乎还是有些不同的,或者说算是其中一部分吧。在某个群看菊苣聊天,感觉场景管理可能是一种处理游戏中大量对象的方式吧。当对象多的时候,设计的人看着会觉得乱,写代码的人写着也会觉得乱,程序运行的时候电脑、主机之类的也会运行得累。
过了好久以后才继续填这个坑,差点忘了之前写到哪里。嗯,应该还有一个结尾,里面打算放一些各种乱七八糟的东西……
比如之前说到程序运行得累,比如弹幕游戏里面,子弹非常多,每一个子弹都要和玩家判断一下有没有碰撞。因为很多子弹有奇怪的形状,所以每一次判断的时候运算都比较复杂。就算是圆形的子弹,还有一些带着平方的计算。因此有一个很不错的方法可以减少这样的计算。先可以在玩家的旁边画一个框,框外面的子弹就因为距离玩家特别远,就可以自动略过,不判断它有没有撞玩家了。然后再慢慢判断框里面的。因为框的形状很规则,是方形的,所以计算碰撞的时候几乎没什么复杂的运算。
以后有什么乱七八糟的东西,可能也会在这里补上……
————————–
(这个人比较懒,拖坑了好久终于大概填完了)