设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 2426|回复: 1
打印 上一主题 下一主题

[交流讨论] RPG Maker MV 的源码研究 十一

[复制链接]

Lv3.寻梦者

梦石
0
星屑
1882
在线时间
1552 小时
注册时间
2013-4-13
帖子
917
跳转到指定楼层
1
发表于 2019-8-22 06:20:12 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有帐号?注册会员

x
先分享一个RM里关于类和继承的知识
比如在RM里声明一个'类'和它的'继承'的方法是这样的

  1. function Window_Base() {
  2.   this.initialize.apply(this, arguments);
  3. }
  4. Window_Base.prototype = Object.create(Window.prototype);
  5. Window_Base.prototype.constructor = Window_Base;
复制代码

我们创建一个'实例'的方法是

  1. const window = new Window_Base()
复制代码

我们首先明确几个概念
1. 函数也是对象
对象具有属性, 单纯来看'prototype'是'Window_Base'对象的一个属性
当然这个属性是特殊的
2. js没有原生的'类'的支持, js是通过原型链的方式实现继承
Window_Base是一个正常的对象, 也是一个函数
和你用其他方式创建一个对象每有本质的区别
js拥有一个被称为原型的属性, __proto__, 它可以是一个对象也可以为空
JS作为一门动态语言, 它的属性是在运行时才被确定的
所以当你试图访问一个对象的属性的时候(比如a.b), 编译器必须去'找'这个属性
如果直接在这个对象中找到了这个属性, 那么就返回这个属性的值
但是如果没有, 它还会尝试在它的原型里查找
如果还是没有, 它就会在原型的原型里查找, 直到查找到或者原型为null
这个就是js实现继承的特性: 原型链, 这一过程也被称之为动态委托
3. new关键字是一个语法糖

我们可以写一个等价的方法来描述这个new

  1. const $new = ($class, ...args) => {
  2.   const result = {
  3.     __proto__: $class.prototype
  4.   }
  5.   $class.call(result, ...args)
  6.   return result
  7. }
复制代码

说一下constructor
每个对象都有一个constructor
它指向对象的构造器, 比如
Object的constructor指向的是Object
数组的constructor指向的是Array
它并不一定是必须的, 没有这一句一样可以正确创建对象
Window_Base.prototype.constructor = Window_Base;
但是如果没有这一句, 就无法通过obj.constructor来判断对象类型了
我们再说一下Object.create(obj)方法
一个最简单的等价函数为

  1. Object.create = function (proto) {
  2.   function F() { }
  3.   F.prototype = proto;
  4.   return new F();
  5. };
复制代码

我们替换成我们之前写的$new, 就等价于

  1. Object.create = function (proto) {
  2.   return { __proto__: proto }
  3. };
复制代码

我们用RM的方式定义两个类

  1. // 这个相当于定义了一个构造函数
  2. function ClassA() {
  3.   this.a = 10
  4. }
  5. // 我们让A类型继承于Object.prototype
  6. ClassA.prototype = Object.create(Object.prototype);
  7. // 等价于 ClassA.prototype = {__proto__: Object.prototype }
  8. // 最后让对象的原型的构造器函数指向ClassA
  9. ClassA.prototype.constructor = ClassA;
  10. // 我们再创建另一个继承于A的类型B
  11. function ClassB() {
  12.   // 相当于super
  13.   ClassA.call(this)
  14.   this.b = 20
  15. }
  16. ClassB.prototype = Object.create(ClassA.prototype);
  17. // 等价于 ClassB.prototype = {__proto__: ClassA.prototype }
  18. ClassB.prototype.constructor = ClassB;
复制代码

那么现在我们来$new(ClassB)并和new ClassB做一个对比
他们的结构是完全一样的:
ClassB {a:10, b: 20} ▼
  a: 10
  b: 20
  __proto__: ClassA ▼                 <= ClassB.prototype
    ► constructor: ƒ ClassB()
    ► __proto__: Object               <= ClassA.prototype

ES5引入了class关键词, 可以看作是上一步骤的语法糖

  1. class ClassB extends ClassA {
  2.   constructor() {
  3.     super()
  4.     this.b = 20
  5.   }
  6. }
复制代码

当然只能用new关键词来实例化, 避免了被错用
new ClassB的结果:
ClassB {a:10, b: 20} ▼
  a: 10
  b: 20
  __proto__: ClassA ▼
    ► constructor: class ClassB
    ► __proto__: Object

对象 -> ClassB.原型 -> ClassA.原型

由于是动态委托, 所以我们可以随时修改原型链
比如:

  1. ClassB.prototype.hello = function () { this.b = 100 }
复制代码

我们来看我们之前的对象自动就发生了变化

ClassB {a:10, b: 20} ▼
  a: 10
  b: 20
  __proto__: ClassA ▼
    ► constructor: class ClassB
    ► hello: ƒ ()
    ► __proto__: Object

同理如果添加
ClassA.prototype.hello = function () { this.a = 100 }
那么hello就会出现在obj.__proto__.__proto__里面
有兴趣可以自己动手试一试
以上是比正文还长的废话
=============================================

第十章

原文再续, 书接上一回
上一回说到Window_Command增加了Command的概念
我们终于可以讲讲水了这么多回才介绍的主角Window_TitleCommand
这个是我们在RM中遇到的第一个菜单


  1. Window_TitleCommand.prototype.initialize = function () {
  2.   Window_Command.prototype.initialize.call(this, 0, 0);
  3.   this.updatePlacement(); // 重置位置
  4.   this.openness = 0; // 初始为未打开状态
  5.   this.selectLast(); // 选择最后的Command
  6. };

  7. // 记忆唤醒大法~
  8. Window_Command.prototype.initialize = function (x, y) {
  9.   this.clearCommandList();
  10.   this.makeCommandList(); // 关注这个
  11.   var width = this.windowWidth();
  12.   var height = this.windowHeight();
  13.   Window_Selectable.prototype.initialize.call(this, x, y, width, height);
  14.   this.refresh();
  15.   this.select(0);
  16.   this.activate();
  17. };

  18. Window_TitleCommand.prototype.updatePlacement = function () {
  19.   this.x = (Graphics.boxWidth - this.width) / 2; // 居中
  20.   this.y = Graphics.boxHeight - this.height - 96; // 使底部距离边缘96像素
  21. };

  22. Window_TitleCommand.prototype.selectLast = function () {
  23.   // 如果有最后的Command, 则选择
  24.   if (Window_TitleCommand._lastCommandSymbol) {
  25.     this.selectSymbol(Window_TitleCommand._lastCommandSymbol);
  26.   } else if (this.isContinueEnabled()) {
  27.     this.selectSymbol('continue');
  28.   }
  29. };
复制代码




注意Window_TitleCommand._lastCommandSymbol是个静态方法
这就是说明它在整个游戏周期一旦被设定就一直有效

  1. Window_TitleCommand.prototype.processOk = function () {
  2.   Window_TitleCommand._lastCommandSymbol = this.currentSymbol();
  3.   Window_Command.prototype.processOk.call(this);
  4. };
复制代码

每当选择一个Command的时候, 这个Command的Symbol就会记录下来
下一次打开标题菜单的时候就会被选中

makeCommandList在Window_Command里是没有实际内容的
这个一半交由各个子类自己实现

  1. Window_TitleCommand.prototype.makeCommandList = function () {
  2.   this.addCommand(TextManager.newGame, 'newGame');
  3.   this.addCommand(TextManager.continue_, 'continue', this.isContinueEnabled());
  4.   this.addCommand(TextManager.options, 'options');
  5. };
  6. // 判断Continue是否可选, 就是判断有没有存档
  7. Window_TitleCommand.prototype.isContinueEnabled = function () {
  8.   return DataManager.isAnySavefileExists();
  9. };
复制代码

又出现了个没排面的TextManager, 无视它
简单看看老熟人DataManager又干了什么吧

  1. DataManager.loadGlobalInfo = function () {
  2.   var json;
  3.   try {
  4.     // 尝试读取global.rpgsave
  5.     json = StorageManager.load(0);
  6.   } catch (e) {
  7.     console.error(e);
  8.     return [];
  9.   }
  10.   // 如果读取成功
  11.   if (json) {
  12.     var globalInfo = JSON.parse(json);
  13.     // 剔除已经不存在的存档
  14.     for (var i = 1; i <= this.maxSavefiles(); i++) {
  15.       if (!StorageManager.exists(i)) {
  16.         delete globalInfo;
  17.       }
  18.     }
  19.     // 返回存档
  20.     return globalInfo;
  21.   } else {
  22.     return [];
  23.   }
  24. };

  25. DataManager.isThisGameFile = function (savefileId) {
  26.   // 读取全局文件
  27.   var globalInfo = this.loadGlobalInfo();
  28.   if (globalInfo && globalInfo[savefileId]) {
  29.     // 如果是全局本地模式
  30.     if (StorageManager.isLocalMode()) {
  31.       return true;
  32.     } else {
  33.       // 否则额外判断一下存档信息
  34.       var savefile = globalInfo[savefileId];
  35.       return (savefile.globalId === this._globalId &&
  36.         savefile.title === $dataSystem.gameTitle);
  37.     }
  38.   } else {
  39.     return false;
  40.   }
  41. };
  42. // 如果有一个存档存在则返回true
  43. DataManager.isAnySavefileExists = function () {
  44.   var globalInfo = this.loadGlobalInfo();
  45.   if (globalInfo) {
  46.     for (var i = 1; i < globalInfo.length; i++) {
  47.       if (this.isThisGameFile(i)) {
  48.         return true;
  49.       }
  50.     }
  51.   }
  52.   return false;
  53. };
复制代码
Window_TitleCommand基本上功能就这些了
不过我们还要稍微回顾一下Scene_Tittle对它的处理

  1. Scene_Title.prototype.createCommandWindow = function () {
  2.   this._commandWindow = new Window_TitleCommand();
  3.   this._commandWindow.setHandler('newGame', this.commandNewGame.bind(this));
  4.   this._commandWindow.setHandler('continue', this.commandContinue.bind(this));
  5.   this._commandWindow.setHandler('options', this.commandOptions.bind(this));
  6.   this.addWindow(this._commandWindow);
  7. };
复制代码


可以看出, handler是在外面定义的, 连接的是Scene_Title本身的方法
这个就是Window和Scene交互的方法了

===============================
最后我们简单练个手
我们想办法给这个菜单加一个退出Command:
  1. // 来我们第一步先重写一下makeCommandList
  2. // 使用大括号是防止里面的变量污染全局
  3. {
  4.   const temp = Window_TitleCommand.prototype.makeCommandList
  5.   Window_TitleCommand.prototype.makeCommandList = function () {
  6.     temp.call(this)
  7.     this.addCommand('退出', 'test_exit');
  8.   };
  9. }
  10. // 然后在修改一下createCommandWindow
  11. Scene_Title.prototype.createCommandWindow = function () {
  12.   this._commandWindow = new Window_TitleCommand();
  13.   this._commandWindow.setHandler('newGame', this.commandNewGame.bind(this));
  14.   this._commandWindow.setHandler('continue', this.commandContinue.bind(this));
  15.   this._commandWindow.setHandler('options', this.commandOptions.bind(this));
  16.   this._commandWindow.setHandler('test_exit', SceneManager.terminate);
  17.   this.addWindow(this._commandWindow);
  18. };
复制代码
收工!

评分

参与人数 3+3 收起 理由
walf_man + 1 塞糖
alpacanist + 1 塞糖
康姆图帕帕 + 1 塞糖

查看全部评分

夏普的道具店

塞露提亚-道具屋的经营妙方同人作品
发布帖:点击这里

Lv2.观梦者

梦石
0
星屑
699
在线时间
81 小时
注册时间
2019-7-23
帖子
17
2
发表于 2019-8-22 09:24:38 | 只看该作者
给大佬膝盖
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2024-5-13 04:38

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

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