赞 | 65 |
VIP | 231 |
好人卡 | 2 |
积分 | 19 |
经验 | 35171 |
最后登录 | 2024-3-30 |
在线时间 | 1552 小时 |
Lv3.寻梦者
- 梦石
- 0
- 星屑
- 1882
- 在线时间
- 1552 小时
- 注册时间
- 2013-4-13
- 帖子
- 917
|
加入我们,或者,欢迎回来。
您需要 登录 才可以下载或查看,没有帐号?注册会员
x
本帖最后由 沉滞的剑 于 2019-8-31 20:16 编辑
废话两件事:
1. 拖更理由: 过载地牢太好玩了, 明日方舟夏活肝死了, 工作进度完不成又加班了...
2. 真实理由: 虽然预告要讲Scene_Map但是发现一些game object代码实在是太臃肿
要分析就必须粘贴上一大堆代码, 有一种流水帐的感觉, 所以我打算重新开一个新坑(哇哦~
一开始只是凭借的热情来打算分析代码, 后来发现没有目标的话很容易迷失方向
之前也介绍了很多js独特的语言技巧, 所以以后默认大家都有一定的JS代码的理解能力了
我们一般在读源码的时候一般有两个思路
一个是如何理解引擎机制, 另一个是如果我们写插件的话, 应该如何修改源码
所以从这次开始以后每篇文章定一个方向, 带入这两个思路, 不过依然会是源码分析的形式
今天讲一个在旧坑里已经讲到过的Window_Command
这次我们要细讲一些容易被忽视的规则和内容, 增加干货少粘代码
( 惊了! 怕不这是个假的楼主! 你把真的楼主怎么样了!
课题1. 玩家是若何在菜单中进行上下选择操作的
0
游戏菜单算是非常自然和简单的的交互行为了
然而就是这么一个看似简单的操作, 实则实现起来是需要考虑很多细节的
它一共经历了大概3个主要阶段:
捕获输入->修改数据->图像重绘
1.1
在RM的世界里, 一切都以一帧为单位进行, 以下的API其实分别在只代表了其在某一帧的状态
Input.isTriggered
Input.isRepeated
Input.isLongPressed
其中isTriggered只关心按下去的第一帧的状态, 之后不会触发
isRepeated 和 isLongPressed还关心上一帧的状态, 持续了多久, 会持续触发
# Side Note
这里还涉及到一个常见的UI交互问题Debouncing
帧对于玩家来说是个极小的单位, 我们随便按一下键盘好几十帧就过去了, 所以必须要有一种算法对输入频率进行制约
一般分为两种, 一种是输入后不立刻反馈, 而是延迟生效, 如果延迟期间有新的输入则取消前面的输入, 开启新的延迟
另外一种是输入后立刻反馈, 但是一段时间内不接收任何输入
在isRepeated里采取的是第二种方法
1.2
Window_Selectable会在自己的update中的processCursorMove里判断键盘/手柄的输入状态
- Window_Selectable.prototype.processCursorMove = function() {
- if (this.isCursorMovable()) {
- var lastIndex = this.index();
- if (Input.isRepeated("down")) {
- this.cursorDown(Input.isTriggered("down"));
- }
- if (Input.isRepeated("up")) {
- this.cursorUp(Input.isTriggered("up"));
- }
- if (Input.isRepeated("right")) {
- this.cursorRight(Input.isTriggered("right"));
- }
- if (Input.isRepeated("left")) {
- this.cursorLeft(Input.isTriggered("left"));
- }
- if (!this.isHandled("pagedown") && Input.isTriggered("pagedown")) {
- this.cursorPagedown();
- }
- if (!this.isHandled("pageup") && Input.isTriggered("pageup")) {
- this.cursorPageup();
- }
- if (this.index() !== lastIndex) {
- SoundManager.playCursor();
- }
- }
- };
复制代码
它监听了上下左右和翻页键是否是isRepeated
其中翻页键的逻辑是通过handler来决定的, 这里只是留了一个接口
当然如果我们需要添加更多的按键判断, 我们就可以参考上下左右键的做法
# Side Note
isCursorMovable必须满足:
- 窗口打开并激活
- 光标锁定_cursorFixed为false
- 光标并不是全选状态
- 可选item>0
1.3
cursorDown, cursorUp等本质上都只是修改index的方法
- Window_Selectable.prototype.cursorDown = function(wrap) {
- var index = this.index();
- var maxItems = this.maxItems();
- var maxCols = this.maxCols();
- if (index < maxItems - maxCols || (wrap && maxCols === 1)) {
- this.select((index + maxCols) % maxItems);
- }
- };
复制代码
# Side Note
不过可能会注意到一点, 为什么这些方法会带一个参数wrap
之前说了isTriggered只会在第一次按下的时候触发一帧
而isRepeated则会周期性执行
所以这个wrap只在用户按下按键的第一帧的时候为true
那这个逻辑有什么用呢?
对于一行或者一列的菜单来说, 如果按住左右或者上下按键不动, 则会一致触发这个函数
但是, 以cursorDown为例, 打开游戏菜单, 在移动到最后一个选项后, 这个时候就不满足index < maxItems - maxCols
因为按下向下键的时候其实就是index+=maxCols, 如果不满足这个条件, 说明已经在最下面的一行了
但是如果你一下一下地按键盘, 而不是一直按住, 你会发现在选择到最后一个选项地时候, 就会直接到上面去了
而这个现象对于一行一列的菜单有效, 而对于类似技能窗口却无效
这个原因就在于这个wrap, 我们看到当wrap为真的时候,
也就是玩家不长按, 而是通过一下一下按触发isTriggered使wrap为true的时候,
如果满足单行单列的条件, 也会继续修改index
而一个取余%操作就会导致出现回到上面的效果
比如你有一个长度为3的菜单, 你在最下面的item时候, 也就是index等于2的时候
那么按下down以后, index+1, 你会得到3 % 3, index就变为0了, 也就是第一个item
反过来, 如果你index=0, 又会发生什么事情呢? 如果index=0, index-1不就是-1了么
RM则用以下方法避免了这种情况: (index - maxCols + maxItems) % maxItems
1.4
至于鼠标的交互方式我之前在旧坑里提到过, 不过这次新坑我打算再简单说明一下
跟选项选择相关的操作
操作1: 点击item, 触发onTouch(true), 如果之前没有被选中则选择这个item, 否则执行'ok'的handler
操作2: 按住并在菜单上拖动, 触发onTouch(false), 只会选中当前位置的item, 并不会执行'ok'的handler
操作3: 按住上下边距的位置, 可以持续触发cursorUp(false)和cursorDown(false)
- Window_Selectable.prototype.processTouch = function() {
- if (this.isOpenAndActive()) {
- if (TouchInput.isTriggered() && this.isTouchedInsideFrame()) {
- this._touching = true;
- this.onTouch(true); // 操作1
- } else if (TouchInput.isCancelled()) {
- if (this.isCancelEnabled()) {
- this.processCancel();
- }
- }
- if (this._touching) {
- if (TouchInput.isPressed()) {
- this.onTouch(false); // 操作2
- } else {
- this._touching = false;
- }
- }
- } else {
- this._touching = false;
- }
- };
- Window_Selectable.prototype.onTouch = function(triggered) {
- var lastIndex = this.index();
- var x = this.canvasToLocalX(TouchInput.x);
- var y = this.canvasToLocalY(TouchInput.y);
- var hitIndex = this.hitTest(x, y);
- if (hitIndex >= 0) {
- if (hitIndex === this.index()) {
- if (triggered && this.isTouchOkEnabled()) {
- this.processOk(); // 操作1
- }
- } else if (this.isCursorMovable()) {
- this.select(hitIndex); // 操作1 && 操作2
- }
- } else if (this._stayCount >= 10) {
- if (y < this.padding) {
- this.cursorUp(); // 操作3
- } else if (y >= this.height - this.padding) {
- this.cursorDown(); // 操作3
- }
- }
- if (this.index() !== lastIndex) {
- SoundManager.playCursor();
- }
- };
复制代码
# Side Note
isTouchedInsideFrame 判断的是是否在整个窗口里, 包括了边距
isContentsArea 判断的则不包括边距
1.5
onTouch中我关心的重点其实是hitTest
- Window_Selectable.prototype.hitTest = function(x, y) {
- if (this.isContentsArea(x, y)) {
- var cx = x - this.padding; // content的相对坐标
- var cy = y - this.padding;
- var topIndex = this.topIndex();
- for (var i = 0; i < this.maxPageItems(); i++) {
- var index = topIndex + i;
- if (index < this.maxItems()) {
- var rect = this.itemRect(index);
- var right = rect.x + rect.width;
- var bottom = rect.y + rect.height;
- if (cx >= rect.x && cy >= rect.y && cx < right && cy < bottom) {
- return index;
- }
- }
- }
- }
- return -1;
- };
复制代码
注意到不在当前page的item是没有人权的, maxPageItem是通过item的高度计算得到的这也就是滚动出现的条件
而获取的itemRect也是通过index来计算的
这就导致了如果我们要设置不规则的布局, 这种死板的计算方式是无法满足我们的需求的
如果按钮排列不是对齐的, 那么上下左右的关系就不能通过计算index序号的方式来确定
需要每一个item都有自己的数据模型, 需要有id, 有邻居的id, 有自己的位置等等
当我们需要设计一个比较灵活的界面的时候, 这个部分可能需要我们花很多时间去处理
1.6
到这里就完成了捕获输入和修改数据的部分
对于图像的绘制, 本质上的问题是Cursor发生了改变
1. 它改变了位置
2. 它会有选中时候的闪烁动画
改变位置:
调用继承于Window的方法
- Window_Selectable.prototype.updateCursor = function() {
- if (this._cursorAll) {
- var allRowsHeight = this.maxRows() * this.itemHeight();
- this.setCursorRect(0, 0, this.contents.width, allRowsHeight);
- this.setTopRow(0);
- } else if (this.isCursorVisible()) {
- var rect = this.itemRect(this.index());
- this.setCursorRect(rect.x, rect.y, rect.width, rect.height); <= 这里
- } else {
- this.setCursorRect(0, 0, 0, 0);
- }
- };
- Window.prototype.setCursorRect = function(x, y, width, height) {
- var cx = Math.floor(x || 0);
- var cy = Math.floor(y || 0);
- var cw = Math.floor(width || 0);
- var ch = Math.floor(height || 0);
- var rect = this._cursorRect;
- if (
- rect.x !== cx ||
- rect.y !== cy ||
- rect.width !== cw ||
- rect.height !== ch
- ) {
- this._cursorRect.x = cx;
- this._cursorRect.y = cy;
- this._cursorRect.width = cw;
- this._cursorRect.height = ch;
- this._refreshCursor();
- }
- };
复制代码
下面是闪烁动画的逻辑
- Window.prototype._updateCursor = function() {
- var blinkCount = this._animationCount % 40;
- var cursorOpacity = this.contentsOpacity;
- if (this.active) {
- if (blinkCount < 20) {
- cursorOpacity -= blinkCount * 8;
- } else {
- cursorOpacity -= (40 - blinkCount) * 8;
- }
- }
- this._windowCursorSprite.alpha = cursorOpacity / 255;
- this._windowCursorSprite.visible = this.isOpen();
- };
复制代码
_updateCursor 在Window的updateTransform中调用, 这个是覆写了PIXI.Container父类的方法
updateTransform会被render调用
这个的完整一帧(requestAnimationFrame)的调用栈如下
SceneManager.update
SceneManager.updateMain
Graphics.render
WebGLRenderer.prototype.render
Container.prototype.updateTransform
Window.prototype.updateTransform
Window.prototype._updateCursor
总结来说updateTransform是会被Graphics.render触发的loop方法
不像update那么直接明了, 不用debugger查看的话很难找到
对我们有价值的结论就是cursor和item的位置是绑定的
我们如果改写了itemRect, 那么光标也是会跟着发生改变的
当然我们要是写不规则的光标, 理论上也是可行的
那么新坑第一个课题就说这么多
只是一个改变光标位置就在代码实现上就是如此复杂
可能之后还会开一个实战的坑
比如制作一个不规则的菜单
似乎还可以加入参考现有著名插件的写法的部分
做成每天(伪)一个你可能不知道的RM小细节这样的感觉?
以后再说吧, 今天先溜了
|
评分
-
查看全部评分
|