Project1
标题:
MV插件开发日志之镶嵌系统
[打印本页]
作者:
沉滞的剑
时间:
2019-9-1 15:30
标题:
MV插件开发日志之镶嵌系统
本帖最后由 沉滞的剑 于 2019-9-1 16:11 编辑
新坑警告, 内容会在主楼持续更新
这次我打算从头开始制作一个最基础的镶嵌系统
是的, 我知道MV已经有很多成熟的镶嵌系统了
但是这并不妨碍我们自己造个轮子自己开心一下
[line]1[/line]
码了一下午的字结果帖子被吞了, 难受...
一点一点补回中
[line]1[/line]
0
所谓镶嵌系统就是将一些组件自行组合并计算最终收益的系统
所能容纳组件的对象我们称之为插槽
插槽所组成的系统我们称之为面板
0.1 没有意见
收益在这个系统内应该是抽象的
我们对使用者如何组织收益的数据结构, 如何实现收益在其他系统(战斗, 制造, 养成)中的效果并不关心
我们只做收益这个数据的搬运工
0.2 数据-视图
数据和界面在逻辑上要分割开来
数据不依赖于界面
界面不依赖于具体的数据类, 而是依赖抽象的数据类
0.3 命名空间
没有什么比全局变量更邪恶的了
通过使用命名空间, 即使声明更多的变量也不会污染到全局了
尽管模块化才是这一问题的终极解决方案
不过目前这样对于我们也是可以接受了
0.4 测试
我想使用代码来测试代码
而且这些代码会被重复测试并且不断积累
如果我们做出了一个修改
测试会帮我们验证之前的逻辑有没有被破坏
这会在我们犯下更大错误前警告我们
0.5 启动: 搭建骨骼
const StoneSlots = {
Constant: {},
Model: {},
View: {},
Test: {}
};
复制代码
StoneSlots 是我们的顶级命名空间
Constant用来存放我们的常量, no magic number, no hard coding!
Model是数据类, 用来表示我们的插槽和组件的状态
View是视图类, 存放Window和Window中可以重复利用的组件
Test是我们存放单元测试的地方
0.6 使用代码块{}和const防止全局污染
{
const { Model } = StoneSlots;
Model.Stone = class {
static getStoneById(id) {
return Model.Stone._stones[id];
}
static register(id, stone) {
if (!Model.Stone._stones) {
Model.Stone._stones = {};
}
Model.Stone._stones[id] = stone;
}
constructor(id, effects) {
this._effects = effects;
Model.Stone.register(id, this);
}
get effects() {
return this._effects;
}
};
}
复制代码
由const声明的变量其作用域仅限于所在的代码块{}之间
所以Model变量并不会污染全局变量
其他一些由const声明的局部函数也是如此
你会发现我们使用了很多ES5和ES6的语法来帮助我们写一些更好理解和便捷的代码
尽管它们看上去和RM自带的代码风格迥异,
但它们看上去使用起来是如此地优雅, 哦, 我亲爱的朋友, 为什么不呢?
0.7 单元测试
我想对我之前写的Model.Stone类进行一个测试:
{
const { Model, Test } = StoneSlots;
...
Test.testModelStone = () => {
const id = 101;
const effects = [{ code: "a", data: "b", value: "c" }];
const stone = new Model.Stone(id, effects);
expectEqual(Model.Stone.getStoneById(id), stone);
expectEquivalent(stone.effects, { effects });
};
...
}
复制代码
testModelStone是一个单元测试方法
它一开始初始化了一些测试环境和对象,
然后用断言判断了它们是否能正常工作
expectEqual和expectEquivalent是我自己定义地两个断言方法, 马上就会讲到
这很好, 但是如果能再输出一些日志信息就更好了
所以我们可以在断言方法中把日志添加进去
{
...
const expectEqual = (tested, expected) => {
assertions.push({
tested: JSON.stringify(tested),
expected: JSON.stringify(expected),
name: "isEqual",
value: tested === expected
});
};
...
}
复制代码
assertions是我们当前测试的日志数组,
注意到我们使用了JSON.stringify, 这不但能将测试对象内部的信息打印出来
还可以防止这些对象在之后被修改导致信息显示错误的问题
好了, 我们现在有了日志信息, 但是每次都要写一次单元测试函数调用实在是太麻烦了
我们写一个方法让所有单元测试可以自动执行
{
const runTestCase = () => {
Object.entries(Test).forEach(([name, test]) => {
setup(name);
test();
log();
});
};
}
复制代码
name是单元测试的名字, test就是每一个单元测试的本身
setup用来初始化日志数组, 并记录一下当前单元测试的名字, 我们打印日志的时候需要用到
log是输出日志的方法
这些方法的具体实现请看2楼
我们来运行一下我们现在的代码
StoneSlots.js:191 Start Test: testModelStone
StoneSlots.js:195 Testing (1/2): success
StoneSlots.js:195 Testing (2/2): success
StoneSlots.js:202 End of Test: testModelStone
复制代码
看起来成功了, 不过如果我们故意让它fail掉呢? 比如改成: expectEquivalent(stone.effects, {});
运行一下, 结果是:
Start Test: testModelStone
StoneSlots.js:195 Testing (1/2): success
StoneSlots.js:195 Testing (2/2): fail
StoneSlots.js:199 Expected isEquivalent: {}, but Recived : [{"code":"a","data":"b","value":"c"}]
StoneSlots.js:202 End of Test: testModelStone
复制代码
很好, 我们的日志解释了我们出错的原因, 我们就可以回去debug了
下一节将介绍如何设计数据类的部分
[line]1[/line]
作者:
沉滞的剑
时间:
2019-9-1 15:35
标题:
MV插件开发日志之镶嵌系统
本帖最后由 沉滞的剑 于 2019-9-5 01:12 编辑
讲一下模型设计部分的内容, 代码贴在下一楼.
================================
1
当一个好的Model很简单, 它只需要干好两件事: 读与写
1.1 概念与行为
当我们写一个Model的时候, 我们是在定义一个概念
镶嵌系统引入了什么概念?
我们至少需要能镶嵌的组件和能被镶嵌的插槽
那么我们就要用代码来描述它们
然后我们继续思考这些对象能做什么, 它们之间有什么关系
镶嵌的主要目的是提供收益, 那么组件就必须记录一些收益效果
在RM中, 开发者一般通过物品注释来引入额外信息
所以我们需要为组件绑定一个物品id
而组件最大的特点就是能够镶嵌进一个插槽
那么镶嵌是一个组件的行为还是插槽的行为呢?
插槽, 因为插槽的作用是管理所存储的组件
而组件不需要知道自身是否被镶嵌了
通过类似这样的分析我们下一步要实现什么内容了
1.2 参数解构
通过参数解构的方式可以忽略函数中参数的顺序
可读性和灵活性都大大增强
一般函数的参数调用方法
> f(a, b) => a - b
> f(10, 5)
5
> f(5, 10)
-5
结构函数的调用方法
> f = ({a, b}) => a - b
> f({a: 10, b: 5})
5
> f({b:5, a: 10})
5
而且不需要担心以后拓展后参数长度很难确定的问题
只要通过参数名来赋值就好了
1.3 静态成员
在OOP中静态成员, 带static关键字的, 是属于类而不属于类的实例的
经常被当作全局变量来使用
一般方法为:
const obj = new Class();
obj.member
而静态成员的使用方法为:
Class.staticMember
我们在这里使用Model.Stone当作一个全局变量
用register记录每一个生成的Stone对象
用getStoneById来读取存储的对象
1.4 getter/setter
之前在旧坑里谈过一个Object.defineProperty的方法
在class中有个类似可以设置getter和setter的方法
比如 get effects() {}
对于一些不会被更改的属性, 我习惯用getter来表明这个值是只读的
而不是把真正的变量暴露给使用者
所有我不想暴露的变量, 我用下划线_开头来表示
1.6 使用常量
之前说了不要硬编码,
我们就引入了Constant这个子空间来存放我们需要用到的常量和枚举类型
OP_CODE记录了Slot装载一个对象返回的状态码
我们能够通过状态码来判断行为的结果和失败时候的原因
1.5 测试
制作一部分, 写一部分测试是一个好习惯
我在Stone中写了一个注册id的方法, 那么我就判断一下是否注册成功了
expectEqual(Model.Stone.getStoneById(id), stone);
我也写了一个getter, 我们来测试一下是否和我们初始化时候设置的一致
expectEquivalent(stone.effects, effects);
Stone是一个没有什么状态变化的类
当测试一个拥有内部状态的类的时候需要覆盖的所有可能清醒
比如测试Slot:
当没有组件时候如果强制卸载和卸载会返回什么状态
当没有组建时候如果装备组件会返回什么状态
当有组件的时候再装备组件的时候会返回什么状态
等等很多case需要考虑到, 一般测试代码都会比实现代码要长的多
1.6 功能拓展
这些功能太基础, 我们想要拓展它们
我们可以用一种设计技巧叫做高阶函数
也就是把一个函数当作参数传递给一个函数, 并返回一个新的函数
可以看成一个工厂, 把原材料放进去, 然后进行了一些加工后输出成一个产品
在JS中类本质上就是函数, 所以我们可以写高阶函数的方法返回一个组装的类
最妙的是这种写法可以叠加
useFeature3(useFeature2(useFeature1(class)))
但是这样实在是太丑了, 我们可以通过reduce方法来链式调用:
features = [useFeature1, useFeature2, useFeature3]
f = baseClass => features.reduce((x, y) => y(x), baseClass)
当然拓展功能也是需要测试的
作者:
沉滞的剑
时间:
2019-9-5 01:13
本帖最后由 沉滞的剑 于 2019-9-8 01:14 编辑
Stone和RM数据的整合
为了制作第一个窗体, 我们需要先将已有Model和RM整合一下
=============================
2
我们要将Stone信息初始化,
但是并不希望用户写大量的脚本, 而是使用物品注释直接生成Stone
好的系统整合如同外科手术: 高效, 微创, 副作用低.
2.1 注释格式
首先我选择RM的标准格式来定义这个物品是不是一个Stone:<StoneSlots-Stone>
Stone初始化有两个参数一个是id, 另一个是effects
因为effects是一个不定格式的序列化对象, 我希望让用户以json的方式来定义
为了更加自由和美观, 我觉得使用开闭标签的形式来让用户定义这个对象
比如<StoneSlots-Effect>{value:123, code:123, data:123}</StoneSlots-Effect>
每一对StoneSlots-Effect标签算是一个effect, 可以允许有多个effect出现
2.1 覆写
首先我们要对数据加载方法进行覆写
覆写原RM方法的标准方式:
const _temp = DataManager.onLoad.bind(DataManager); // 必须绑定this指针
DataManager.onLoad = function(object) {
temp(object)
... // 我们在这里就可以插入我们的逻辑了
}
我们针对object === $dataItems时再进行一轮额外的处理
2.2 匹配注释
首先我们需要一个能匹配形如<xxx>yyy</xxx>的正则表达式,
因为以后可能还会有其他的开闭标签, 所以我们这里选择匹配任意成对的标签名
/<(.*?)>(.*?)<\/\1>/g
其中*?是非贪婪模式的记号
\1是匹配第一个捕捉内容的记号
让后我们对第二个捕获内容使用Json.parse成为对象
这个内容可以是字符串"123", 数字123, 数组[123, "123"],和对象{123:123}
2.3 创建对象
对象的id可以用item.id获得, effects从上面的匹配后的信息中获得
这时候就可以直接new Model.Stone({id, effects}) 来创建并注册实例了
这里我又为Model.Stone添加了两个静态方法
getAllStone()只是返回已注册的所有stone
getStoneItems(items)是筛选形如{物品id: 物品数}中所有是Stone的物品
比如getStoneItems($gameParty._items)来获取所队伍所持有的Stone物品
由于Model不应该依赖于RM对象, 所以$gameParty._items就用参数的方法传入了
作者:
沉滞的剑
时间:
2019-9-8 01:14
当前代码, 持续更新中
//=============================================================================
// StoneSlots.js
//=============================================================================
/*:
* @plugindesc 基础镶嵌系统
* @author Heartcase
*
* @help
*
* 所谓镶嵌系统就是将一些组件自行组合并计算最终收益的系统
* 所能容纳组件的对象我们称之为插槽
* 插槽所组成的系统我们称之为面板
*
* 本系统对只提供计算收益的接口
* 对收益的具体逻辑和结构并没有意见
*
* 本系统只提供最基础和核心的功能
* 其他功能可以通过拓展本插件获得
*/
/**
* 全局命名空间
*/
const StoneSlots = {
Constant: {},
Model: {},
View: {},
Test: {}
};
/**
* 常量
*/
{
const { Constant } = StoneSlots;
/**
* 操作返回值
*/
Constant.OP_CODE = {
SUCCESS: 0,
NOT_EMPTY: 1,
EMPTY: 2
};
}
/**
* 数据类型
*/
{
const { Model, Constant } = StoneSlots;
const { OP_CODE } = Constant;
/**
* 组件类
*/
Model.Stone = class {
static getStoneById(id) {
return Model.Stone._stones[id];
}
static register(id, stone) {
if (!Model.Stone._stones) {
Model.Stone._stones = {};
}
Model.Stone._stones[id] = stone;
}
static getAllStone() {
return Model.Stone._stones;
}
static getStoneItems(items) {
const obj = {};
Object.entries(items)
.filter(([index, _]) => {
return Object.keys(Model.Stone._stones).includes(index);
})
.forEach(([index, nums]) => {
obj[index] = nums;
});
return obj;
}
constructor({ id, effects }) {
this._effects = effects;
Model.Stone.register(id, this);
}
get effects() {
return this._effects;
}
};
/**
* 插槽类
*/
Model.Slot = class {
constructor() {
this._stone = null;
}
get stone() {
return this._stone;
}
isEmpty() {
return !this._stone;
}
equip(stone, safe = false) {
if (safe && !this.isEmpty()) {
return OP_CODE.NOT_EMPTY;
}
this._stone = stone;
return OP_CODE.SUCCESS;
}
unequip(safe = false) {
if (safe && this.isEmpty()) {
return OP_CODE.EMPTY;
}
this._stone = null;
return OP_CODE.SUCCESS;
}
};
/**
* 面板类
*/
Model.SlotCollection = class {
constructor({ slotList }) {
this._slotList = slotList;
}
getSlot(index) {
return this._slotList[index];
}
getTotalEffects() {
return this._slotList
.map(slot => slot.effects)
.reduce((x, y) => x.concat(y), []);
}
};
/**
* 高阶函数
*/
Model.useLockedSlot = Slot => {
return class extends Slot {
constructor({ locked, ...restArgs }) {
super(restArgs);
this._locked = locked;
}
isLocked() {
return this._locked;
}
lock() {
this._locked = true;
}
unlock() {
this._locked = false;
}
};
};
}
/**
* 界面
*/
{
const { View } = StoneSlots;
View.Command_Slot = class {};
View.Window_SlotCollection = class {};
View.Window_StoneList = class {};
}
/**
* RM 整合
*/
{
const _temp = DataManager.onLoad.bind(DataManager);
// $1: 标签名
// $2: 内容
const re = /<(.*?)>(.*?)<\/\1>/g;
const { Constant, Model } = StoneSlots;
Constant.META_STONE = "StoneSlots-Stone";
Constant.META_EFFECT = "StoneSlots-Effect";
DataManager.onLoad = function(object) {
_temp(object); //
if (object === $dataItems) {
object
// 去除0号null值
.slice(1)
.filter(each => each.meta && each.meta[Constant.META_STONE])
.forEach(each => {
const id = each.id;
const effects = [];
// 移除换行
const note = each.note.replace(/\n/g, "");
let matches;
while ((matches = re.exec(note))) {
switch (matches[1]) {
case Constant.META_EFFECT:
effects.push(JSON.parse(matches[2]));
}
}
new Model.Stone({ id, effects });
});
}
};
}
/**
* 测试
*/
{
const { Constant, Model, Test } = StoneSlots;
const { OP_CODE } = Constant;
let assertions;
let testName;
// 断言方法
const expectEqual = (tested, expected) => {
assertions.push({
tested: JSON.stringify(tested),
expected: JSON.stringify(expected),
name: "isEqual",
value: tested === expected
});
};
const expectTrue = tested => {
return expectEqual(tested, true);
};
const expectNotTrue = tested => {
return expectEqual(tested, false);
};
const isEquivalent = (objA, objB) => {
if (typeof objA != "object" || typeof objB != "object") {
return objA === objB;
}
if (Object.keys(objA).length !== Object.keys(objB).length) {
return false;
}
return Object.keys(objA).every(key => {
if (typeof objA[key] === "object") {
if (typeof objB[key] === "object") {
return isEquivalent(objA[key], objB[key]);
} else {
return false;
}
} else {
return objA[key] === objB[key];
}
});
};
const expectEquivalent = (tested, expected) => {
assertions.push({
tested: JSON.stringify(tested),
expected: JSON.stringify(expected),
value: isEquivalent(tested, expected),
name: "isEquivalent"
});
};
// 测试周期
const setup = name => {
assertions = [];
testName = name;
};
const log = () => {
console.log(`Start Test: ${testName}`);
const length = assertions.length;
assertions.forEach((assertion, index) => {
const { tested, expected, value, name } = assertion;
console.log(
`Testing (${index + 1}/${length}): ${value ? "success" : "fail"}`
);
if (!value) {
console.log(`Expected ${name}: ${expected}, but Recived : ${tested} `);
}
});
console.log(`End of Test: ${testName}`);
};
const runTestCase = () => {
Object.entries(Test).forEach(([name, test]) => {
setup(name);
test();
log();
});
};
// 测试
Test.testModelStone = () => {
const id = 101;
const effects = [{ code: "a", data: "b", value: "c" }];
const stone = new Model.Stone({ id, effects });
const items = { 101: 123, 999: 456 };
const stones = { 101: 123 };
expectEqual(Model.Stone.getStoneById(id), stone);
expectEquivalent(stone.effects, effects);
expectEquivalent(Model.Stone.getAllStone(), { 101: stone });
expectEquivalent(Model.Stone.getStoneItems(items), stones);
};
Test.testModelSlot = () => {
const slot = new Model.Slot();
const id = 101;
const effects = [{ code: "a", data: "b", value: "c" }];
const stone = new Model.Stone({ id, effects });
// 没有装备组件时
expectTrue(slot.isEmpty());
expectEqual(slot.stone, null);
expectEqual(slot.unequip(true), OP_CODE.EMPTY);
expectEqual(slot.unequip(), OP_CODE.SUCCESS);
// 装备组件时
expectEqual(slot.equip(stone, true), OP_CODE.SUCCESS);
expectEqual(slot.equip(stone, true), OP_CODE.NOT_EMPTY);
expectEqual(slot.equip(stone), OP_CODE.SUCCESS);
expectNotTrue(slot.isEmpty());
expectEqual(slot.stone, stone);
// 卸下组件时
expectEqual(slot.unequip(true), OP_CODE.SUCCESS);
expectTrue(slot.isEmpty());
};
Test.testUseLockedSlot = () => {
const testClass = class {};
const featuredClass = Model.useLockedSlot(testClass);
const instance = new featuredClass({ locked: false });
expectNotTrue(instance.isLocked());
instance.lock();
expectTrue(instance.isLocked());
instance.unlock();
expectNotTrue(instance.isLocked());
};
runTestCase();
}
复制代码
作者:
累文龙
时间:
2020-3-25 13:04
那个,,问一下,你这创建一个槽的意思是设置一个‘透明’的物品,让装备合宝石组合在一起?假设分为三个阶级,从小到大,第三阶级是整个透明的物品,第二阶级是装备。第一阶级是宝石
作者:
累文龙
时间:
2020-3-25 13:06
镶嵌,用读取到的装备,宝石,即两个对象,保留装备这个对象,宝石这个对象则只吸纳它的属性,附加给装备这个对象
作者:
累文龙
时间:
2020-3-25 13:08
我想请问的是,怎么给装备这个对象去识别宝石,也就是说,宝石给装备的使用方法?
作者:
累文龙
时间:
2020-3-25 13:08
楼主你的代码,,没看懂全部,ES6。。。
作者:
累文龙
时间:
2020-3-25 13:09
楼主你的代码,,没看懂全部,ES6。。。
欢迎光临 Project1 (https://rpg.blue/)
Powered by Discuz! X3.1