铁血丹心

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

[lua复刻] 【新手向】lua复刻版0.4源码解析(三)枯燥的开始游戏

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

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

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

x
本帖最后由 决定吃小菜 于 2022-11-29 05:11 编辑

源码解析:


数据就是这么的枯燥乏味,你能爱多久,MOD你还想继续做下去吗?







1.嵌套菜单
讲了两期,画面也都还没动过,这次我们大胆()选择故事模式。
很惊喜,很刺激,还是没变,只有三行文字变了。

赶快翻代码瞅瞅,搜索‘新的旅程’
  1. function Normal_Game()
  2.     Cls();
  3.     CC.EndLess = false;
  4.     CC.LoadThingPic = 0
  5.     CC.MainSubMenuX = CC.MainMenuX + 2 * CC.MenuBorderPixel + 2 * CC.FontBig + 5; --主菜单为两个汉字
  6.     CC.MainSubMenuX2 = CC.MainSubMenuX + 2 * CC.MenuBorderPixel + 4 * CC.FontBig + 5; --子菜单为四个字符
  7.     CC.SingleLineHeight = CC.FontBig + 2 * CC.MenuBorderPixel + 5; --带框的单行字符高

  8.     local menu = { { "新的旅程", nil, 1 },
  9.                    { "旧日足迹", nil, 1 },
  10.                    { "迷途知返", nil, 1 } };
复制代码
Normal_Game()函数里,本着函数执行一定是有人调用了的原则我们再搜索关键字‘Normal_Game
  1. function StartMenu()
  2.     local menu = { { "故事模式", Normal_Game, 1 },
  3.                    { "挑战模式", Endless_Game, 1 },
  4.                    { "归隐山林", nil, 1 } };
  5.     local menux = (CC.ScreenW - 4 * CC.FontBig * 3) / 2

  6.     local menuReturn = ShowMenu(menu, #menu, 0, menux, CC.ScreenH - 4 * CC.FontBig, 0, 0, 0, 0, CC.FontBig, C_STARTMENU, C_RED)

  7.     if menuReturn == 3 then
  8.         JY.Status = GAME_END;
  9.     end

  10. end
复制代码
回到了熟悉的开始菜单,Normal_Game在嵌套table的里面。
{}这种大括号包着的是lua语言中的table,我们用它来组织数据结构。

在解析ShowMenu通用函数时我并没有对参数进行说明,因为源码里注释的很详细,但这里还是要再说一下。


一个菜单table包含了文字描述、选择后要执行的函数、是否显示三个数据,然后多个菜单又被外层table包住。
lua语言允许函数作为参数传进另一个函数里,然后在需要的时候用()去调用

ShowMenu中以如下方式调用随菜单传进来的函数,注释代码后再次运行游戏发现按回车后已经没反应。
  1. local r = newMenu[current][2](newMenu, current); --调用菜单函数
复制代码


那这个函数里又调ShowMenu就形成嵌套菜单,前面代码看到菜单‘新的旅程’附带的菜单函数是nil,表示我没有需要执行的函数。假如给替换成StartMenu()就会无限循环这两组菜单。
nil是lua语言预设的关键字,表示【空】【没有】,因为ShowMenu里已经规定那个位置的参数就是一个函数,你不可以放别的,而你却没有函数可以执行时就把nil传过去告诉ShowMenu函数,这是一种信息的表达方式

把测试代码删掉,让我们真正开始新游戏。






2.新的游戏

Normal_Game函数里一眼望去,还真没什么新鲜东西
  1. function Normal_Game()
  2.     Cls();
  3.     CC.EndLess = false;
  4.     CC.LoadThingPic = 0
  5.     CC.MainSubMenuX = CC.MainMenuX + 2 * CC.MenuBorderPixel + 2 * CC.FontBig + 5; --主菜单为两个汉字
  6.     CC.MainSubMenuX2 = CC.MainSubMenuX + 2 * CC.MenuBorderPixel + 4 * CC.FontBig + 5; --子菜单为四个字符
  7.     CC.SingleLineHeight = CC.FontBig + 2 * CC.MenuBorderPixel + 5; --带框的单行字符高

  8.     local menu = { { "新的旅程", StartMenu, 1 },
  9.                    { "旧日足迹", nil, 1 },
  10.                    { "迷途知返", nil, 1 } };
  11.     local menux = (CC.ScreenW - 4 * CC.FontBig) / 2

  12.     local menuReturn = ShowMenu(menu, 3, 0, menux, CC.ScreenH - 4 * CC.FontBig, 0, 0, 0, 0, CC.FontBig, C_STARTMENU, C_RED)

  13.     if menuReturn == 1 then
  14.         --重新开始游戏
  15.         Cls();
  16.         DrawString(menux, CC.ScreenH - 5 * CC.StartMenuFontSize, "请稍候...", C_RED, CC.StartMenuFontSize);
  17.         ShowScreen();

  18.         NewGame(); --设置新游戏数据
复制代码


现在看这种代码应该没有难度了吧,熟悉的通用菜单函数,熟悉的文字绘制DrawString,熟悉的屏幕刷新函数,最后NewGame()没有见过,搜索定位它的位置。
  1. function NewGame()
  2.     --选择新游戏,设置主角初始属性

  3.     if #CC.NewPersonName < 2 or #CC.NewPersonName > 8 then
  4.         DrawStrBoxWaitKey("主角名限定一到四个汉字", C_WHITE, CC.DefaultFont, 1);
  5.         DrawStrBoxWaitKey("请退出游戏后重新设定", C_WHITE, CC.DefaultFont, 1);
  6.         return 0
  7.     end

  8.     local key = -1
  9.     local mjzz = 0

  10.     LoadRecord(0); -- 载入新游戏数据
复制代码
先只看这一部分,DrawStrBoxWaitKey方法也是很简单,里面也是之前解析过的文字绘制+等待按键的逻辑组合。
往下看又遇到一个新鲜函数LoadRecord(0),注释说这是载入新游戏数据用的,定位到它一起看看游戏究竟是怎么从文件加载到内存里的。


2.1 存档文件

存档中存放的是会被玩家影响而改变的数据,像是主角的等级、学到的武功、剧情进度、物品等。
金群游戏本体可看作一个不可被玩家修改覆盖的存档,开始新游戏就是读取它。一起来看看都加载了什么数据。
原函数很长,照例截取一点一点看。
  1. function LoadRecord(id)
  2.     -- 读取游戏进度

  3.     local t1 = lib.GetTime();

  4.     --读取R*.idx文件
  5.     local data = Byte.create(6 * 4);
  6.     Byte.loadfile(data, CC.R_IDXFilename[0], 0, 6 * 4);

  7.     local idx = {};
  8.     idx[0] = 0;
  9.     for i = 1, 6 do
  10.         idx[i] = Byte.get32(data, 4 * (i - 1));
  11.     end
复制代码
lib.GetTime()新的lib函数,获取了一个时间,有什么用目前还不清楚,接着往下看先。
Byte.create(6 * 4)Bytelib一样也是由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循环
  1. JY.Person[0]["内力性质"] = Rnd(2);
复制代码
  1. function Rnd(i)
  2.     --随机数
  3.     local r = math.random(i);
  4.     return r - 1;
  5. end
复制代码
出现了非常多人物属性初始化的代码,随机函数是自己封装的修改了一下范围,这也是一个开发技巧,当你发现别人的函数满足不了你就可以做一层封装,可千万别写成
  1. JY.Person[0]["内力性质"] = math.random(2) - 1;
复制代码
这样应付了事是非常坏的开发习惯。


往下看到绘制出文字的地方,local function ... end,定义了一个函数内的函数,内部函数可以方便的使用外部函数的变量。
里面还是熟悉的lib.FillColor()和DrawString(),像表格一样排列属性,对满上限的再画一个背景块(果然是因为lib.background坏了么)


  1. ShowScreen();

  2.         local menu = { { "是 ", nil, 1 },
  3.                        { "否 ", nil, 2 },
  4.         };
  5.         local ok = ShowMenu2(menu, 2, 0, x1 + 11 * fontsize, CC.NewGameY - CC.MenuBorderPixel, 0, 0, 0, 0, fontsize, C_RED, C_WHITE)
  6.         if ok == 1 then
  7.             break ;
  8.         end
复制代码
每选一次否就随机新的数据并刷新画面。ShowMenu2的作用就是上一期提到的横向菜单。
函数命名是一门科学,尽量做到能自释,最好一看就能知道是干吗用的,例如:ShowMenu_Horizontal


选是后跳出循环,程序回到NewGame()继续执行。


2.3 绘制场景
  1. lib.ShowSlow(50, 1)
  2.         JY.Status = GAME_SMAP;
  3.         JY.MmapMusic = -1;

  4.         CleanMemory();

  5.         Init_SMap(0);
复制代码
50毫秒间隔变化从亮到暗,50并不是总的时间,是控制变化频率。
第二行游戏状态改为进入场景了。
CleanMemory()回收一下不再使用的内存。
Init_SMap(0)初始化场景地图,定位此函数看看内部实现。


  1. --初始化场景数据
  2.     lib.PicInit();
  3.     lib.PicLoadFile(CC.SMAPPicFile[1], CC.SMAPPicFile[2], 0);

  4.     lib.PicLoadFile(CC.HeadPicFile[1], CC.HeadPicFile[2], 1)
  5.     if CC.HeadPicFile[1] == CONFIG.DataPath .. "hore.idx" and CC.HeadPicFile[2] == CONFIG.DataPath .. "hore.grp" then
  6.         lib.PicLoadFile(CC.HeadPicFile[1], CC.HeadPicFile[2], 1, math.modf(45 * CONFIG.Zoom / 100))
  7.     end

  8.     if CC.LoadThingPic == 1 then
  9.         lib.PicLoadFile(CC.ThingPicFile[1], CC.ThingPicFile[2], 2);
  10.     end
复制代码
一上来就是一堆c++实现,都是绘制场景地图需要的资源,包括人物行走图,场景贴图,人物头像,物品贴图。把图片先放进内存,后面绘制速度就会很快。


  1. PlayMIDI(JY.Scene[JY.SubScene]["进门音乐"]);
复制代码
播放场景地图背景音乐,是由存档数据决定的,对,就是上面LoadRecord()方法里加载的存档数据,现在我们知道了存档里存放了场景数据。


DrawSMap()绘制场景地图,虽然这个函数很长,但我们只关注几行就可以。
  1. function DrawSMap(fastdraw)

  2.     local x0 = JY.SubSceneX + JY.Base["人X1"] - 1; --绘图中心点
  3.     local y0 = JY.SubSceneY + JY.Base["人Y1"] - 1;

  4.     --场景移动视角限制
  5.     local x = limitX(x0, 12, 45) - JY.Base["人X1"];
  6.     local y = limitX(y0, 12, 45) - JY.Base["人Y1"];

  7.         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()后面还有一小段代码
  1. lib.ShowSlow(50, 0)
  2.     lib.GetKey();

  3.     if showname == 1 then
  4.         DrawStrBox(-1, 10, JY.Scene[JY.SubScene]["名称"], C_WHITE, CC.DefaultFont);
  5.         ShowScreen();
  6.         WaitKey();
  7.         Cls();
  8.         ShowScreen();
  9.     end
复制代码
绘制地图以后逐渐亮起画面,后面紧跟一个lib.GetKey()
lib.GetKey()还有一个隐藏作用,让程序变得有响应。程序无响应的窗口都遇到过吧?
当传参showname等于1时还会绘制场景名,并且WaitKey(),这就是每次切换场景还要再按一下按键才能控制的原因,反正就是讨厌
Init_SMap()会多次调用,例如大地图进入场景,从战斗场景回到普通场景等。


总结起来它的任务就是加载场景需要的资源,播放BGM,绘制地图和角色,然后用场景名卡你一下

函数执行完毕,回到调用处NewGame(),新的游戏开始了,要不要观看开场剧情?


还是留到下次观看吧,下次来解析一下事件的运作。






3.总结


本来想一周更新两次,终究败给了懒癌
开始游戏最难理解的部分应该就是加载存档数据了,但是这部分涉及了很多二进制的知识,思虑再三决定还是跳过。
之前论坛还有朋友问到,如何把二进制的存档数据改为lua静态数据,我当时也没有详细回答,一是因为我能力有限,二是因为需要具备二进制相关知识,而你如果具备相关知识也一定是大佬了。


这里先简单说一下好了,假如你有很多本书放在一个箱子里,你要如何阅读随机一本书的某一段文字?很简单啊,我知道那本书在箱子里的位置,以及知道那段文字在那本书的位置就可以啊(感觉是废话


存档就是这样的,罗列的二进制数据你知道所有数据的位置关系就可以轻松处理,转lua静态数据也就顺理成章的事。


所以,数据是枯燥的,也没有太多解析的必要,要了解数据结构可以看UPedit的配置文件。






【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
发表于 2022-11-30 17:11 | 显示全部楼层
我认为你从JY.Person系列的个人属性开始教会比较好,像你前几篇帖提到的菜单函数和绘图对新手并不友好。

今天这篇帖还提到存读档的问题,Byte系列还要去算数据库的位子,看这些东西除了老手都会很蛋疼。

存读档也没什么学习的必要,除非是想玩各记录档的主角大乱斗,一般新手感兴趣的还是改改个人属性。

改改武功、天赋、剧情,或是扩充数据库之类的,新手也不适合碰战斗系统,有先后顺序问题。
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
 楼主| 发表于 2022-11-30 22:01 | 显示全部楼层
woabclf 发表于 2022-11-30 17:11
我认为你从JY.Person系列的个人属性开始教会比较好,像你前几篇帖提到的菜单函数和绘图对新手并不友好。

...

确实,没考虑那么多
单纯就是想从入口开始运行的流程顺下来。
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
发表于 2022-12-2 22:31 | 显示全部楼层
感觉比老孙的可读性高一点
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。
发表于 2023-12-2 02:44 | 显示全部楼层
請教具體 LUA固定 段落 可以微信嗎,指點一下 我研究得太頭痛了.微信alu3266
【武侠.中国】铁血丹心论坛(大武侠):致力于推广和发展武侠文化,让我们一起努力,做全球最大的武侠社区。
可能是目前为止最好的金庸群侠传MOD游戏交流论坛,各种经典武侠游戏等你来玩,各种开源制作工具等你来实现你的游戏开发之梦。

本版积分规则

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

GMT+8, 2024-11-17 20:43

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2021, Tencent Cloud.

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