本帖最后由 决定吃小菜 于 2022-11-29 05:11 编辑
数据就是这么的枯燥乏味,你能爱多久,MOD你还想继续做下去吗?
1.嵌套菜单
讲了两期,画面也都还没动过,这次我们大胆()选择故事模式。
很惊喜,很刺激,还是没变,只有三行文字变了。
赶快翻代码瞅瞅,搜索‘新的旅程’
- function Normal_Game()
- Cls();
- CC.EndLess = false;
- CC.LoadThingPic = 0
- CC.MainSubMenuX = CC.MainMenuX + 2 * CC.MenuBorderPixel + 2 * CC.FontBig + 5; --主菜单为两个汉字
- CC.MainSubMenuX2 = CC.MainSubMenuX + 2 * CC.MenuBorderPixel + 4 * CC.FontBig + 5; --子菜单为四个字符
- CC.SingleLineHeight = CC.FontBig + 2 * CC.MenuBorderPixel + 5; --带框的单行字符高
- local menu = { { "新的旅程", nil, 1 },
- { "旧日足迹", nil, 1 },
- { "迷途知返", nil, 1 } };
复制代码 在Normal_Game()函数里,本着函数执行一定是有人调用了的原则我们再搜索关键字‘Normal_Game’
- function StartMenu()
- local menu = { { "故事模式", Normal_Game, 1 },
- { "挑战模式", Endless_Game, 1 },
- { "归隐山林", nil, 1 } };
- local menux = (CC.ScreenW - 4 * CC.FontBig * 3) / 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
复制代码 回到了熟悉的开始菜单,Normal_Game在嵌套table的里面。
{}这种大括号包着的是lua语言中的table,我们用它来组织数据结构。
在解析ShowMenu通用函数时我并没有对参数进行说明,因为源码里注释的很详细,但这里还是要再说一下。
一个菜单table包含了文字描述、选择后要执行的函数、是否显示三个数据,然后多个菜单又被外层table包住。
lua语言允许函数作为参数传进另一个函数里,然后在需要的时候用()去调用
ShowMenu中以如下方式调用随菜单传进来的函数,注释代码后再次运行游戏发现按回车后已经没反应。
- local r = newMenu[current][2](newMenu, current); --调用菜单函数
复制代码
那这个函数里又调ShowMenu就形成嵌套菜单,前面代码看到菜单‘新的旅程’附带的菜单函数是nil,表示我没有需要执行的函数。假如给替换成StartMenu()就会无限循环这两组菜单。
nil是lua语言预设的关键字,表示【空】【没有】,因为ShowMenu里已经规定那个位置的参数就是一个函数,你不可以放别的,而你却没有函数可以执行时就把nil传过去告诉ShowMenu函数,这是一种信息的表达方式
把测试代码删掉,让我们真正开始新游戏。
2.新的游戏
Normal_Game函数里一眼望去,还真没什么新鲜东西
- function Normal_Game()
- Cls();
- CC.EndLess = false;
- CC.LoadThingPic = 0
- CC.MainSubMenuX = CC.MainMenuX + 2 * CC.MenuBorderPixel + 2 * CC.FontBig + 5; --主菜单为两个汉字
- CC.MainSubMenuX2 = CC.MainSubMenuX + 2 * CC.MenuBorderPixel + 4 * CC.FontBig + 5; --子菜单为四个字符
- CC.SingleLineHeight = CC.FontBig + 2 * CC.MenuBorderPixel + 5; --带框的单行字符高
- local menu = { { "新的旅程", StartMenu, 1 },
- { "旧日足迹", nil, 1 },
- { "迷途知返", nil, 1 } };
- local menux = (CC.ScreenW - 4 * CC.FontBig) / 2
- local menuReturn = ShowMenu(menu, 3, 0, menux, CC.ScreenH - 4 * CC.FontBig, 0, 0, 0, 0, CC.FontBig, C_STARTMENU, C_RED)
- if menuReturn == 1 then
- --重新开始游戏
- Cls();
- DrawString(menux, CC.ScreenH - 5 * CC.StartMenuFontSize, "请稍候...", C_RED, CC.StartMenuFontSize);
- ShowScreen();
- NewGame(); --设置新游戏数据
复制代码
现在看这种代码应该没有难度了吧,熟悉的通用菜单函数,熟悉的文字绘制DrawString,熟悉的屏幕刷新函数,最后NewGame()没有见过,搜索定位它的位置。
- function NewGame()
- --选择新游戏,设置主角初始属性
- if #CC.NewPersonName < 2 or #CC.NewPersonName > 8 then
- DrawStrBoxWaitKey("主角名限定一到四个汉字", C_WHITE, CC.DefaultFont, 1);
- DrawStrBoxWaitKey("请退出游戏后重新设定", C_WHITE, CC.DefaultFont, 1);
- return 0
- end
- local key = -1
- local mjzz = 0
- LoadRecord(0); -- 载入新游戏数据
复制代码 先只看这一部分,DrawStrBoxWaitKey方法也是很简单,里面也是之前解析过的文字绘制+等待按键的逻辑组合。
往下看又遇到一个新鲜函数LoadRecord(0),注释说这是载入新游戏数据用的,定位到它一起看看游戏究竟是怎么从文件加载到内存里的。
2.1 存档文件
存档中存放的是会被玩家影响而改变的数据,像是主角的等级、学到的武功、剧情进度、物品等。
金群游戏本体可看作一个不可被玩家修改覆盖的存档,开始新游戏就是读取它。一起来看看都加载了什么数据。
原函数很长,照例截取一点一点看。
- function LoadRecord(id)
- -- 读取游戏进度
- local t1 = lib.GetTime();
- --读取R*.idx文件
- local data = Byte.create(6 * 4);
- Byte.loadfile(data, CC.R_IDXFilename[0], 0, 6 * 4);
- local idx = {};
- idx[0] = 0;
- for i = 1, 6 do
- idx[i] = Byte.get32(data, 4 * (i - 1));
- end
复制代码 lib.GetTime()新的lib函数,获取了一个时间,有什么用目前还不清楚,接着往下看先。
Byte.create(6 * 4),Byte跟lib一样也是由c++端注入的全局变量,挂载了很多操作字节的方法,Byte.create()函数按照给的参数申请内存空间。
要把数据从硬盘文件读到内存,第一步都是申请内存空间,最佳做法是文件数据有多少就申请多少 Byte.loadfile(data, file, start, length),第一个参数就是刚刚申请到的内存空间,第二个参数是文件路径,第三参数表示读取起始位置,第四参数表示读取长度。
这看起来复杂,但如果你站在设计者角度就明白了,它是可以读取一个文件任意位置的数据,是对所有文件都通用的。
接着往下看。
Byte.get32(data, start),获取一个整数,32代表占内存32位,还有get8、get16两种,这里涉及到计算机是如何用二进制表示整数的知识,希望你有此基础。因为长度是32位也就是4个字节,只需要传起始位置就可以了,等效于Byte.get32(data, start, 4)
那么这一整段代码究竟做了什么,暂时不知道
而且后面都是类似的代码,头大!我决定不解析了,之后有需要再看。
2.2 属性随机骰子
回到NewGame()函数来,往下又进入一个while循环
- JY.Person[0]["内力性质"] = Rnd(2);
复制代码- function Rnd(i)
- --随机数
- local r = math.random(i);
- return r - 1;
- end
复制代码 出现了非常多人物属性初始化的代码,随机函数是自己封装的修改了一下范围,这也是一个开发技巧,当你发现别人的函数满足不了你就可以做一层封装,可千万别写成
- JY.Person[0]["内力性质"] = math.random(2) - 1;
复制代码 这样应付了事是非常坏的开发习惯。
往下看到绘制出文字的地方,local function ... end,定义了一个函数内的函数,内部函数可以方便的使用外部函数的变量。
里面还是熟悉的lib.FillColor()和DrawString(),像表格一样排列属性,对满上限的再画一个背景块(果然是因为lib.background坏了么)
- ShowScreen();
- local menu = { { "是 ", nil, 1 },
- { "否 ", nil, 2 },
- };
- local ok = ShowMenu2(menu, 2, 0, x1 + 11 * fontsize, CC.NewGameY - CC.MenuBorderPixel, 0, 0, 0, 0, fontsize, C_RED, C_WHITE)
- if ok == 1 then
- break ;
- end
复制代码 每选一次否就随机新的数据并刷新画面。ShowMenu2的作用就是上一期提到的横向菜单。
函数命名是一门科学,尽量做到能自释,最好一看就能知道是干吗用的,例如:ShowMenu_Horizontal
选是后跳出循环,程序回到NewGame()继续执行。
2.3 绘制场景
- lib.ShowSlow(50, 1)
- JY.Status = GAME_SMAP;
- JY.MmapMusic = -1;
- CleanMemory();
- Init_SMap(0);
复制代码 50毫秒间隔变化从亮到暗,50并不是总的时间,是控制变化频率。
第二行游戏状态改为进入场景了。
CleanMemory()回收一下不再使用的内存。
Init_SMap(0)初始化场景地图,定位此函数看看内部实现。
- --初始化场景数据
- lib.PicInit();
- lib.PicLoadFile(CC.SMAPPicFile[1], CC.SMAPPicFile[2], 0);
- lib.PicLoadFile(CC.HeadPicFile[1], CC.HeadPicFile[2], 1)
- if CC.HeadPicFile[1] == CONFIG.DataPath .. "hore.idx" and CC.HeadPicFile[2] == CONFIG.DataPath .. "hore.grp" then
- lib.PicLoadFile(CC.HeadPicFile[1], CC.HeadPicFile[2], 1, math.modf(45 * CONFIG.Zoom / 100))
- end
- if CC.LoadThingPic == 1 then
- lib.PicLoadFile(CC.ThingPicFile[1], CC.ThingPicFile[2], 2);
- end
复制代码 一上来就是一堆c++实现,都是绘制场景地图需要的资源,包括人物行走图,场景贴图,人物头像,物品贴图。把图片先放进内存,后面绘制速度就会很快。
- PlayMIDI(JY.Scene[JY.SubScene]["进门音乐"]);
复制代码 播放场景地图背景音乐,是由存档数据决定的,对,就是上面LoadRecord()方法里加载的存档数据,现在我们知道了存档里存放了场景数据。
DrawSMap()绘制场景地图,虽然这个函数很长,但我们只关注几行就可以。
- function DrawSMap(fastdraw)
- local x0 = JY.SubSceneX + JY.Base["人X1"] - 1; --绘图中心点
- local y0 = JY.SubSceneY + JY.Base["人Y1"] - 1;
- --场景移动视角限制
- local x = limitX(x0, 12, 45) - JY.Base["人X1"];
- local y = limitX(y0, 12, 45) - JY.Base["人Y1"];
- lib.DrawSMap(JY.SubScene, JY.Base["人X1"], JY.Base["人Y1"], x, y, JY.MyPic);
复制代码 首先确立了地图中心,然后又是一个c++实现。
lib.DrawSMap(场景id,主角坐标x,主角坐标y,地图中心x,地图中心y,主角行走图),为了保证绘制速度,场景地图的绘制交给了c++。
如果你用UPedit看过贴图资源就会发现地图都是一个一个小方块拼起来的,c++内写了一套很复杂的算法拼合地图块,营造斜视角度的效果,而且绘制定义了与屏幕坐标系不同的坐标概念,简单说就是一个小方块就是一个坐标单位。
至于为何需要传地图中心坐标,是因为只绘制围绕中心一定范围的地图可以节省计算资源。
人物位置自然是为了把人物贴图画在那里。
其他代码没必要看了,根本不会执行,都是过时无效的
Init_SMap()后面还有一小段代码
- lib.ShowSlow(50, 0)
- lib.GetKey();
- if showname == 1 then
- DrawStrBox(-1, 10, JY.Scene[JY.SubScene]["名称"], C_WHITE, CC.DefaultFont);
- ShowScreen();
- WaitKey();
- Cls();
- ShowScreen();
- end
复制代码 绘制地图以后逐渐亮起画面,后面紧跟一个lib.GetKey()
lib.GetKey()还有一个隐藏作用,让程序变得有响应。程序无响应的窗口都遇到过吧? 当传参showname等于1时还会绘制场景名,并且WaitKey(),这就是每次切换场景还要再按一下按键才能控制的原因,反正就是讨厌
Init_SMap()会多次调用,例如大地图进入场景,从战斗场景回到普通场景等。
总结起来它的任务就是加载场景需要的资源,播放BGM,绘制地图和角色,然后用场景名卡你一下
函数执行完毕,回到调用处NewGame(),新的游戏开始了,要不要观看开场剧情?
还是留到下次观看吧,下次来解析一下事件的运作。
3.总结
本来想一周更新两次,终究败给了懒癌
开始游戏最难理解的部分应该就是加载存档数据了,但是这部分涉及了很多二进制的知识,思虑再三决定还是跳过。
之前论坛还有朋友问到,如何把二进制的存档数据改为lua静态数据,我当时也没有详细回答,一是因为我能力有限,二是因为需要具备二进制相关知识,而你如果具备相关知识也一定是大佬了。
这里先简单说一下好了,假如你有很多本书放在一个箱子里,你要如何阅读随机一本书的某一段文字?很简单啊,我知道那本书在箱子里的位置,以及知道那段文字在那本书的位置就可以啊(感觉是废话
存档就是这样的,罗列的二进制数据你知道所有数据的位置关系就可以轻松处理,转lua静态数据也就顺理成章的事。
所以,数据是枯燥的,也没有太多解析的必要,要了解数据结构可以看UPedit的配置文件。
|