本帖最后由 决定吃小菜 于 2022-11-29 05:13 编辑
我们的目标是读懂每一行代码。
1.代码回顾
上期我们止步于标题按钮的文字修改,再回顾一下代码先:
- function StartMenu()
- local menu = { { "故事模式", Normal_Game, 1 },
- { "挑战模式", Endless_Game, 1 },
- { "归隐山林", nil, 1 } };
- local menux = (CC.ScreenW - 4 * CC.FontBig) / 2
- local menuReturn = ShowMenu(menu, #menu, 0, menux, CC.ScreenH - 4 * CC.FontBig, 0, 0, 0, 0, CC.FontBig, C_STARTMENU, C_RED)
- if menuReturn == 3 then
- JY.Status = GAME_END;
- end
- end
复制代码- menu变量记录了三个选项
- menux变量只是获得了一堆复杂计算的结果,是按钮x轴的起始位置。
- menuReturn变量记录我们今天的主角ShowMenu方法的结果
最后的if...end代码块让游戏状态变成了结束,于是游戏程序跳出循环,窗口关闭。
程序运行完毕后就会被操作系统回收资源,所以游戏都是运行在一个死循环中阻止程序运行完,只有需要关闭时才会主动跳出死循环
2.ShowMenu
如果你已经用上了Intellij IDEA社区版+EmmyLua插件,那么按住Ctrl+鼠标左键点击ShowMenu会自动跳转方法定义处。
如果你的编辑器没有这种跳转功能还是要用搜索方式找到ShowMenu。
- function ShowMenu(menuItem, numItem, numShow, x1, y1, x2, y2, isBox, isEsc, size, color, selectColor)
复制代码
源码我就不贴了,这个方法非常非常的长要不咱们就到这。。。。不过源码里也给了非常详细的注释,如果有能力凭注释说明看动内部代码那也的确没必要再看此贴了。
其他人,我们继续来一步一步解析。
大体上可以分为绘制逻辑和控制逻辑
2.1 绘制逻辑
- function ShowMenu4(menuItem, numItem, numShow, x1, y1, x2, y2, isBox, isEsc, size, color, selectColor)
- ...(省略若干代码)
- local surid = lib.SaveSur(0, 0, CC.ScreenW, CC.ScreenH);
- while true do
- if num ~= 0 then
- lib.LoadSur(surid, 0, 0);
- if isBox == 1 then
- DrawBox(x1, y1, x1 + w, y1 + h, C_WHITE);
- end
- end
- for i = start, start + num - 1 do
- local drawColor = color; --设置不同的绘制颜色
- if i == current then
- drawColor = selectColor;
- lib.Background(x1 + CC.MenuBorderPixel, y1 + CC.MenuBorderPixel + (i - start) * (size + CC.RowPixel),
- x1 - CC.MenuBorderPixel + w, y1 + CC.MenuBorderPixel + (i - start) * (size + CC.RowPixel) + size, 128, color)
- end
- DrawString(x1 + CC.MenuBorderPixel, y1 + CC.MenuBorderPixel + (i - start) * (size + CC.RowPixel),
- newMenu[i][1], drawColor, size);
- end
- if JY.Status == GAME_START then
- DrawString(10, 10, "金庸群侠传『SRPG』复刻原版 v0.41", C_ORANGE, CC.Fontsmall)
- DrawString(10, 10 + CC.Fontsmall + CC.RowPixel, "MOD移植:苍天泰坦", C_ORANGE, CC.Fontsmall)
- DrawString(10, 10 + (CC.Fontsmall + CC.RowPixel) * 2, "挑战模式作者:JY027", C_ORANGE, CC.Fontsmall)
- DrawString(10, 10 + (CC.Fontsmall + CC.RowPixel) * 3, "企鹅交流群:63627774", C_ORANGE, CC.Fontsmall)
- end
- ShowScreen();
- keyPress = WaitKey(1);
- lib.Delay(100);
-
- end
- ...(省略若干代码)
- end
复制代码
上面一堆local开头的变量声明可以先不看,其中有一行lib.GetKey()其实放这里没有任何意义,想做代码优化时可以删掉。
尽可能的删除了不相干的代码。
这里进入了一个while条件循环,条件是true就是一个死循环,一直无限循环执行内部代码,直到我们主动跳出。
这样的逻辑就保证了在标题页面玩家可以一直对三个按钮做控制。
而跟随控制按钮的样式也发生了变化,所以绘制逻辑也放在了循环里,保证你按一下变一次。
2.1.1 画面缓存的利用
- lib.SaveSur 缓存画面,返回给你一张id存折(不是
- lib.LoadSur 读取画面,递出存折,把当时存的画面还给我
- lib.FreeSur 这个画面我也不需要了,丢掉吧
lib.SaveSur参数也是非常经典的起始坐标和宽高,坐标我们熟悉了,都是0那就是屏幕左上角的原点。CC开头的是游戏中会经常用到的数据,通过它我们很方便的拿到了游戏窗口的宽CC.ScreenW,高CC.ScreenH。那么,第一行代码里我们把整个窗口做了一次快照缓存
没错,就是很朴素的一张背景图而已,因为在调用ShowMenu之前,我们只调用了一个绘制代码lib.LoadPicture,还记得是哪里调用的吗?
之所以这样做,都是为了实现“按一下按钮改变一次画面”。
代码没有任何智慧,所有行为都是开发者在控制,你脑中去想“按方向键下,第二个选项高亮”是不行的,我们需要确切的用代码告诉电脑“清理窗口里所有像素,普通方式画第一个选项,以高亮方式画第二个选项,普通方式画第三个选项”
你可能会疑惑,为什么要清理所有像素,不是只需要改第二个选项高亮吗?
现在,你就要建立一个思维,每一帧都是独立绘制的,绝对绝对不要在帧与帧之间建立状态关系。这看起来是浪费性能,其实不然,你如果记录状态去修改绘制局部,你会发现你将陷入逻辑深渊无法自拔。
无法理解也没关系,只需要记住,直接刷新画板再画是最能保证代码效率和安全的方式。
既然每一帧都要清空画面再画那三个按钮,作为一个通用函数,你是不知道之前的画面是什么样子,如果一起清掉了结果就是窗口里一片漆黑,只留下三个大按钮。
为了防止这个情况我们就调用lib.SaveSur把当前已经绘制的画面缓存下来,每次重新画按钮之前把它画上。
2.1.2 画面帧数
说起帧,应该都理解就是一秒内画面刷新了几次。
- while true do
- ShowScreen();
- keyPress = WaitKey(1);
- lib.Delay(100);
- end
复制代码
我们已经处在了一个死循环内,其他任何代码都不会执行了,所以此时游戏有多少帧,取决于这个while运行有多快。
其他的代码都是微秒级的执行速度,我们可以忽略不计。
- ShowScreen()的作用就是把我们画好的画板提交到显示器上来,也就是调一次就是一帧,调用之前屏幕上的像素不会有任何变化。
- WaitKey(1)就是等待按键,这个留到控制逻辑的部分再详细解析。
- lib.Delay(100)休息100毫秒,100毫秒后往下执行。
基于以上可以判断,调用ShowMenu方法后,游戏的帧数是不确定的,取决于玩家按键频率,但最快也是10帧,因为有一个100毫秒休息的限制。
2.1.3 画选项
- for i = start, start + num - 1 do
- local drawColor = color; --设置不同的绘制颜色
- if i == current then
- drawColor = selectColor;
- lib.Background(x1 + CC.MenuBorderPixel, y1 + CC.MenuBorderPixel + (i - start) * (size + CC.RowPixel),
- x1 - CC.MenuBorderPixel + w, y1 + CC.MenuBorderPixel + (i - start) * (size + CC.RowPixel) + size, 128, color)
- end
- DrawString(x1 + CC.MenuBorderPixel, y1 + CC.MenuBorderPixel + (i - start) * (size + CC.RowPixel),
- newMenu[i][1], drawColor, size);
- end
复制代码
绘制逻辑最主要的部分来了,真正把按钮画出来就是这段代码。
在我省略的代码中,做了一系列计算来确定按钮的位置,一个窗口说大不大,但你要把你的按钮摆在正确的坐标上还是需要考虑很多的,尤其如果是通用函数,要考虑更多变数,例如:选项数量变化的影响,选项文字大小的影响,排版方式的影响等等。
我之所以省略,实在觉得解析没有意义,坐标系的计算都是初中就学的东西
for循环次数就是依据按钮个数,能画几个按钮就循环几次,具体多少次怎么计算出来的,其实也没必要太关心,不做无意义解析。
lib.Background(x1,y1,x2,y2,bright,color) 是一个绘制颜色的底层实现,比lib.FillColor多了一个bright参数,其他参数是一样的用法,bright是亮度,取值范围0-255。突然发现这个方法并没有起作用,没有画出应该画的颜色,没关系用其他开源mod时肯定是有效果的。
DrawString(x, y, str, color, size, flag)用来绘制文字,其内部调用lib.DrawStr底层实现,为了实现同一行文字有不同颜色的效果做了封装。参数都很好理解,坐标,文字,颜色,大小,只有最后一个flag没啥用,直接不传即可。
2.1.4 版本信息
- if JY.Status == GAME_START then
- DrawString(10, 10, "金庸群侠传『SRPG』复刻原版 v0.41", C_ORANGE, CC.Fontsmall)
- DrawString(10, 10 + CC.Fontsmall + CC.RowPixel, "MOD移植:苍天泰坦", C_ORANGE, CC.Fontsmall)
- DrawString(10, 10 + (CC.Fontsmall + CC.RowPixel) * 2, "挑战模式作者:JY027", C_ORANGE, CC.Fontsmall)
- DrawString(10, 10 + (CC.Fontsmall + CC.RowPixel) * 3, "企鹅交流群:63627774", C_ORANGE, CC.Fontsmall)
- end
复制代码 绘制逻辑剩余的部分就是标题界面看到的左上角的版本信息,不喜欢的可以删掉
2.2 控制逻辑
如果没有控制逻辑,画面永远都是定死的。控制逻辑如同点名,老师每一帧都看一遍全班同学的名单(老师好累),然后想点谁就点谁,只有点到的名的请站起来这种概念。
- while true do
- for i = start, start + num - 1 do
- if i == current then
- drawColor = selectColor;
- end
- end
- keyPress = WaitKey(1);
-
- if keyPress == VK_ESCAPE then
- if isEsc == 1 then
- break
- end
- elseif keyPress == VK_DOWN then
- current = current + 1;
- elseif keyPress == VK_UP then
- current = current - 1;
- end
- end
复制代码
我们暂时省略绘制逻辑的代码,保留控制逻辑相关代码。
for循环里,谁高亮是current决定的,我们控制的就是current的值。
三个按钮,那么就让current在1到3之间变化,是多少就会让第几个按钮亮起来。
WaitKey()方法是阻塞式方法,即使你是死循环,运行到这里也要乖乖停下来,一直等到有玩家按下按键才能继续往下循环下去。你想到了什么吗,没错它内部也是一个死循环,一个等待玩家按键才能跳出的死循环。
拿到按键值之后做了一系列判断,如果是“下”current 加1,如果是“上”current 减1,左右方向我们没必要处理,按下回车则可以跳出循环,并返回给调用者选择的值是多少,调用者再根据返回值判断玩家选了哪个,再做进一步的逻辑处理。
搜索VK_UP定义来源,你还会发现其他很多键盘按键的定义,26个字母和常用功能键都有了。
代码中其他的888,999等逻辑就不看了,或者是特殊的确认方式,或者是对选项坐标y值的控制,还是那句话,当你想做这种逻辑时自然会写的,强行读懂别人的没有意义,基础的控制逻辑理解后就没有什么效果实现不了。
3.总结
这次我们从头到尾解析了一个通用函数,之后会不断的看到它的出现。解析完后对帧绘制这件事有了更深入的理解,对你以后子定制画面会有极大帮助。
那么不如我就留一个小作业,看看你是否真的掌握了如何在想要的位置上画出想画的东西
请尝试实现以下效果:
1.菜单按钮改横向布局,横向上居中
2.按左右进行选择,选中时有边框
一些提示:
- DrawBox()绘制边框
- 横向时文字高度相等,只有x坐标在变化
- 完整复制ShowMenu方法并重命名为ShowMenu5,自己修改着玩的时候不要随意更改原代码
- lib.Background方法可能无效,我们先不使用这个方法了。
|