站内搜索

搜索
热搜: 活动 交友 discuz

iFAction 世界需要你的想象力!

【脚本上手指南】从前端JS到iFAction

8

主题

59

帖子

669

积分

iF村庄

Rank: 5Rank: 5

积分
669

摩点支持者内测用户

发表于 2020-11-28 14:24:48 | 显示全部楼层 |阅读模式
本帖最后由 MCCF 于 2020-11-28 14:24 编辑


从前端JS到iFAction

一、前言
作为目前解释速度最快的脚本语言之一,JavaScript已经越来越得到普及,也出现了各种各样的书籍。
然而,iFAction与通常所使用的前端JS有一定差距,也并没有提供前端所需的一些函数,属于自成一派的脚本体系。
这使得想要买书、上课学习的学习者在学习之后发现内容难以与实现挂钩,使得iFAction脚本的门槛增高,
必须弄懂整个脚本体系才能开始上手编辑MOD或脚本。
为了解决这一问题,似乎迫切需要一个教程,专门讲解在已有前端JS基础上快速转化成iFAction的MOD实现的方法。
本教程如有疏漏在所难免,欢迎指正。

二、需求JS水平
变量、数组、赋值、运算等操作√
分支、循环结构√
函数及其熟练运用√
构造函数与面向对象√
良好编码习惯及规范√

三、提示
1. 建议将脚本拆解到文档,并使用专业代码编辑器,如Sublime Text或VSCode编辑代码,可大幅提升效率。
2. 本文中“实例”指某个构造函数构造的对象,Func#prop表示Func这一构造函数的prop这一属性(对象/函数),
例如DSet#setEvent表示DSet这一构造函数中用this.setEvent定义的所生成对象的属性。
3. 善于运用全局搜索。利用全局搜索“顺藤摸瓜”的方式找到自己想要的接口或属性是最高效的学习方法,之后会有详细讲解(大概)。
4. 建议定义MOD专属对象,并确保其它生成的全局变量/函数皆是这个对象的属性。由于只有一个对象,所有属性用这个
对象调用,因此可以大大减小出现命名冲突的问题。例如:
  1. var AutoAimingMod = {};
  2. AutoAimingMod.processData = function(data){ ... };
  3. ...
  4. AutoAimingMod.processData(someData);
复制代码

8

主题

59

帖子

669

积分

iF村庄

Rank: 5Rank: 5

积分
669

摩点支持者内测用户

 楼主| 发表于 2020-11-28 14:24:49 | 显示全部楼层
本帖最后由 MCCF 于 2021-2-10 23:19 编辑


架构篇

(大佬可跳过本篇)
首先,制作MOD,通常是一个“修改”或“重定义”原生代码的过程,因此首先需要了解iFAction内部脚本的架构。
一、概论
iFAction脚本分为以下九个模块:
Control 控件 Data 数据库数据 Game 游戏进程数据 Init 初始化
Interpreter 事件解释器 Logic 逻辑对象 Root 通用对象 Scene 场景 Window 可视化界面

以上模块,互相调用,各有分工,形成了完善的体系。
下面以此为线索进行讲解。
二、简析Data
Data,顾名思义即是数据之意。它的内容最多,但是最为容易理解。
它处理的主要是游戏中“设置”、“变量”、“资源”、“界面”部分的数据,
例如DResScene处理“资源”(Resource)中“场景”(Scene)的部分,DSetActor处理“设置”(Setting)中“角色”(Actor)的部分。
可以发现这一部分的命名存在规律,可以依据翻译软件等自己探索。
下面以DSetInteractionBlock(设置->交互块)为例讲解。
  1. function DSetInteractionBlock(rd){
  2.     //是否是物品交互块
  3.     this.isItem = false;
  4.     //可被放在在块内部
  5.     this.isImplant = false;
  6.     //拥有重力
  7.     this.isGravity = true;
  8.     //可穿透的
  9.     this.isPenetrate = false;
  10.     //拥有改变形态
  11.     this.isStatus = false;
  12.     //死亡块
  13.     this.isDie = false;
  14.     //可破坏
  15.     this.isDestroy = false;
  16.     //弹跳
  17.     this.isJump = false;
  18.     //消失
  19.     this.isVanish = false;


  20.     //增加金钱
  21.     this.money = 0;
  22.     //增加生命
  23.     this.hpValue = 0;
  24.     //增加法力
  25.     this.mpValue = 0;
  26.     //增加最大生命
  27.     this.maxHpValue = 0;
  28.     //增加最大法力
  29.     this.maxMpValue = 0;
  30.     //增加命数
  31.     this.leftValue = 0;

  32.     this.id = rd.readShort();
  33.     //名称
  34.     this.name = rd.readString();
  35.     //交互块对应“资源”中的图块ID
  36.     this.BlockId = rd.readShort();
  37.     //被破坏后对应的图块ID
  38.     this.BlockId2 = rd.readShort();
  39.     //执行通用触发器的ID
  40.     this.EventId = rd.readShort();

  41.     //破坏播放动画ID
  42.     this.animId = rd.readShort();
  43.     //角色播放动画ID
  44.     this.actorAnimId = rd.readShort();

  45.     this.isItem = rd.readBool();

  46.     this.isGravity = rd.readBool();
  47.     this.isPenetrate = rd.readBool();
  48.     this.isStatus = rd.readBool();

  49.     if (this.isItem) {
  50.         this.money = rd.readInt();
  51.         this.hpValue = rd.readInt();
  52.         this.mpValue = rd.readInt();
  53.         this.maxHpValue = rd.readInt();
  54.         this.maxMpValue = rd.readInt();
  55.         this.leftValue = rd.readInt();
  56.     }else {
  57.         this.isDie = rd.readBool();
  58.         this.isDestroy = rd.readBool();
  59.         this.isJump = rd.readBool();
  60.         this.isVanish = rd.readBool();

  61.         this.isImplant = rd.readBool();
  62.     }

  63.     this.cState = [];

  64.     var length = rd.readInt();
  65.     for (var i = 0; i < length; i++) {
  66.         this.cState[rd.readShort()] = rd.readShort();
  67.     }
  68. }
复制代码

rd是IRWFile的对象,用于打开并读取二进制数据库文件的信息。
可以发现此处定义了大量的属性,实际上就是所读取的一个交互块的各项设定信息。
例如DSetInteractionBlock#isGravity,对应的就是在设置中设定的单个交互块是否拥有重力的这一设定。
而rd.readBool()就是从数据文件中读取一个Bool值(1/0)来获取该交互块是否拥有重力这一信息,填充给isGravity。
对比下图,也许可以对Data部分产生更清晰的认识。

那么具体如何调用到这一个部分呢?
观察DSet及SStart,会发现其中有:
  1. this.setBlock = [];
复制代码
  1. length = rd.readInt();
  2. for(i = 0;i<length;i++){
  3.     temp = new DSetInteractionBlock(rd);
  4.     _sf.setBlock[temp.id] = temp;
  5. }
复制代码
  1. RV.NowSet = new DSet(function(){
  2.     load += 1;
  3. });
复制代码

于是,我们就可以用RV.NowSet.setBlock,来调取一个DSetInteractionBlock的数组,
其中RV.NowSet.setBlock[x]就是ID为x的交互块的数据信息。
这样,就实现了DSetInteractionBlock的完整功能。
一些总结:
1. Data部分主要处理数据库类的内容,脚本命名存在规律。
2. Data部分主要有两个核心对象:RV.NowRes和RV.NowSet分别对应DRes和DSet。
3. DRes和DSet中都有设置项目的数组,并且存在规律:将某个设置项目的数组首字母大写,在其前加上D,
即是该数组对应的每个元素的构造函数,例如resScene是DResScene实例的数组,setItem是DSetItem的数组。
4. DSetAll是设置-总体设置部分的对应实例,用RV.NowSet.setAll调用,包含有重力系数、初始命数等数据。
5. 另外还有DBlock、DMap等内容也包含在Data部分中,可以自己探索。
6. 似乎是由于小雨的疏忽,数组的名称经常会打错(如resBBlock)但是懂的都懂。

二、简析Game
Game是游戏进行过程中的数据。
注意此处的“游戏过程”是指在“开始游戏”或“继续游戏”后的游戏数据,会随着读档等而改变。
比如,角色的金币数就是存储在GMain部分中的,而角色的血量、BUFF存储在GActor中。
如果发生读档或重开游戏,即使游戏程序不关闭,这些数据也会重置或重设。
我们来观察GMain的一部分:
  1. function GMain(){
  2.     //角色数据信息
  3.     this.actor = null;
  4.     //剩余命数
  5.     this.life = 0;
  6.     //变量
  7.     this.value = [];
  8.     //独立开关
  9.     this.selfSwitch = [];
  10.     //当前的地图Id
  11.     this.mapId = 0;
  12.     //当前x坐标
  13.     this.x = 0;
  14.     //当前y坐标
  15.     this.y = 0;
  16.     //当前朝向
  17.     this.dir = 0;
  18.     //金钱
  19.     this.money = 0;
  20.     //背包物品
  21.     this.items = [];
  22.     //快捷栏的技能
  23.     this.userSkill = [0,0,0,0,0,0,0,0,0,0];
  24.     //快捷栏的物品
  25.     this.userItem = [0,0,0,0,0,0,0,0,0,0];
  26.     //控制角色是否拥有重力
  27.     this.isGravity = true;
  28.     //重力系数
  29.     this.gravityNum = 0;
  30.     //跳跃系数
  31.     this.jumpNum = 0;
  32.     //跳跃段数
  33.     this.jumpTimes = 0;
  34.     //控制角色是否可穿透
  35.     this.isCanPenetrate = false;
  36.     //步数
  37.     this.step = 0;
  38. ...
复制代码

可以发现,诸如变量、背包、金钱、快捷栏、当前地图、角色坐标等诸多信息,
都是只存在于某个特定存档中的,每次读档都会刷新的数据。

这些数据的调用也十分简单。RV.GameData即是当前存档的GMain实例,
RV.GameData.actor则是当前的GActor实例,同理还有RV.GameSet但较少使用。
我们主要使用的是GMain以及GActor,其中GMain的使用更多更广。
除了读取和修改外,存读档、获取/丢弃/使用背包物品、获取变量值都需要通过GMain。

调用RV.GameData.save()可以存档,RV.GameData.load()可以读档。
可以通过注入这两个方法来增加存档中存储自己想存储的数据,之后会详细讲解。
获取第id号变量的值可以使用RV.GameData.value[id],其中开关开为true,关为false。
也可以利用RV.GameData.getValue(id,value)设置当id号变量不存在时返回默认值value。
RV.GameData.getValueNum(id,value)则设定在id号变量不存在或不是数值时返回value。
合理利用这两个特殊的获取变量值函数可以精简代码。

其中背包物品在GMain中有一专门的类操作:DBagItem。GMain#items是其实例的数组。
一项物品(即新版背包界面中的一栏)在iFA中有三个属性:
类型(道具0,武器1,防具2)、ID(在对应类型的“设置”数据库中的ID)、数量。
例如现有铁剑2把,则其对应DBagItem的类型为1,ID为1,数量为2。
要区分DBagItem与DSetItem,前者为游戏背包中所用,后者为数据库中的项目。
之后,我们就可以用GMain#addItem(type,id,num)获取指定物品若干个,
用GMain#useItem(id,num,type)使用指定物品若干个。
另外,GMain#discardItem(item,num)可以丢弃物品,其中item为DBagItem的实例。
(不知道小雨为啥三个方法要用三种参数顺序QAQ)

未完待续……

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

8

主题

59

帖子

669

积分

iF村庄

Rank: 5Rank: 5

积分
669

摩点支持者内测用户

 楼主| 发表于 2021-2-10 23:30:26 | 显示全部楼层
本帖最后由 MCCF 于 2021-2-12 21:53 编辑


底层篇 P1

一、概论
起因是一位官方群的大佬不知道怎么把前端的贪吃蛇转化为iFA的贪吃蛇。
于是发现实际上虽然文档是基本完善,但是能够灵活运用大佬的毕竟还是不多,于是来水一发底层篇。
底层实际上也就是iFA所提供的JS形式API接口,这些接口是组成iFA内部功能的基本单位。

对于不同平台,iF2D所能调用的系统底层功能显然有差异,因此需要统一成同样的接口。
例如对于IBitmap.CBitmap这一构造函数,在Web端可以通过FileReader等前端API完成,
在PC端则需要利用C++实现读取、处理位图的功能,最后将一切统一成IBitmap.CBitmap的形式。
所以,底层是iFA脚本编程的起点及核心。很多时候实现许多功能不需要使用底层,
但是为了理解默认脚本的实现并灵活修改,实现包括但不限于倒影、镜子、小地图等效果,
则需要对底层有充分的掌握。

本篇的编写依据是iF2D文档。具体来说,根据以下标准分类讨论:
获取部分 IInput, IVal, IWeb, IRWFile
显示部分 IBitmap, IBCof, ISprite, IViewport
逻辑部分 IRect, IColor, IFont
组件部分 IAnim, IParticle, IButton, ICheck, IScrollbar
其它部分 IAudio, IDll, IDllObject

二、显示部分
显示部分是iF2D最重要的部分,也是进行一定规模修改的基石。
显示部分控制了iF2D画面上包括但不限于角色、敌人、触发器、图块、背景、UI等物件的显示,
且组件部分实际上也是利用显示部分API进行扩展的结果。

1. 位图
位图可以理解为一张“图像”。该图像是以像素点为基础的点阵形成的图像。
换言之,每个像素点可以用四个八位二进制数表示,即RGBA表示法。由这些像素图构成的矩阵就是一张图片。
位图也是游戏中最常用的图像表示方法之一,相比矢量图更灵活,适合裁剪等行为。缺点是渲染速度有时比矢量图低。
例如一个单块图块可以理解成32*32或48*48的位图,而一个自动元件图块可以理解成284*288的位图(宽度乘高度)。

位图在iF2D中存储和使用的基础接口是IBitmap。这不是一个类而是一个API接口,由以下三个函数组成。
这三个函数都会返回一个Image类的实例。具体可以参见文档。

IBitmap.ABitmap(path): 读取一张以工程根目录为基础的路径为path的位图,并返回从该位图生成的Image对象。
要点:path开头是/可能会引起未定义的行为,也就是可能被误认为绝对路径而从网站根目录/磁盘根目录寻找导致错误。
用例:IBitmap.ABitmap("Graphics/System/icon_event.png") //如文件存在且有效,返回一个IBitmap实例,如不存在返回null

IBitmap.WBitmap(path): 读取一张Web端的路径为path的位图,并返回从该位图生成的Image。
要点:由于网络方面的限制,本函数只在Web端有效,其它端同ABitmap。
用例:IBitmap.ABitmap("https://s2.ax1x.com/2019/08/01/ea5j4H.jpg") //在Web端使用,如文件存在且有效,则返回对应实例

IBitmap.CBitmap(width,height): 创建一张全新的指定宽高的空白位图,返回其Image。
要点:主要用于生成利用drawBitmap显示内容的精灵,例如地图图层的精灵是一个图块一个图块绘制的,所以需要默认空白。
用例:IBitmap.CBitmap(114,514) //生成一张恶臭位图(x)

以上ABitmap及WBitmap函数已经在默认iFA脚本中被统一成RF.LoadBitmap(path)。并且以上函数读取的位图都是一次性的。
所以,也可以使用RF.LoadCache(path),该函数对于重复的图像不会重复读取从而提高了效率,
但是要注意不能释放用这个函数读取的位图,例如使用ISprite#disposeMin而不是ISprite#dispose。
另外,上述两个RF下的函数所读取的实际路径为"Graphics/" + path,所以开头也不能出现"/"。

这一接口不提供操作位图的功能,而需要将对位图的操作在ISprite和IViewport执行,以便提高效率。
默认Image主要只需要使用到两个属性width和height,用于获取位图的尺寸从而进行动态的剪裁、位置等处理。

2. 剪裁位图
有时候我们并不希望显示一张图片的全部,而是希望动态地显示图片的局部。
这就需要使用IBCof位图剪裁对象。这一对象的构造函数为IBCof(bitmap,x,y,width,height)。
例如,已经有一个正确读取的位图bitmap,这时,可以使用new IBCof(bitmap,0,0,48,48)生成剪裁位图。
这样生成的剪裁位图的内容将是原位图以(0,0)为左上角,(47,47)为右下角的一个局部内容,宽高均为48。
IBCof可以和Image一样在大部分场景使用。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

18

主题

177

帖子

1916

积分

iF城镇

Rank: 6Rank: 6

积分
1916

摩点支持者内测用户继续加油哦!

QQ
发表于 2021-1-20 17:39:24 | 显示全部楼层
好贴好贴  适合萌新  大佬继续啊
坐等开罗mod
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

联系我们
QQ群:977585123
iFAction下载
Windows客户端
反馈
意见建议
iFAction

iFAction

京ICP备15053274号-1

Powered by Discuz! X3.4 © 2001-2013 Comsenz Inc.