永远庭

Domus Hominis Ludentis

0%

RMMZ-从0开始构建自己的UI(1)

原生的UI实在是太丑了,而且对触屏、滑动等的支持也不太好,我们需要重新构建一个。

功能构想

我期望的UI主要有窗口和按钮构成,窗口至少要有普通窗口、可滚动窗口、可滚动列表窗口(列表item可交互)等几个类型。
对于滚动窗口,还需要有滚动条,以及滚动惯性(快速滚动后松开鼠标,减速滑动一段距离直到停止),滚动极限反馈(超过滚动极限时会“弹”回来),列表Item长按功能(比如显示详情)

原型

以上一些功能窗口,和按钮都有一个共同点,就是可以点击交互,点击还要包括长按和非长按,以及按住滚动等,为此可建一个类叫 Clickable, 为了让它有 transform,可以作为 child 添加给别人我们要继承 PIXI.Container,先写一个简单的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Clickable extends PIXI.Container {
constructor(x = 0, y = 0, w = 0, h = 0) {
super();
this.x = x;
this.y = y;

//宽和高必不可少
this._width = w;
this._height = h;

//是否处于激活(可交互)状态
this._active = true;
}

get width() {return this._width;}
get w() {return this._width;} //宽的简称,为了以后可以偷懒
get height() {return this._height;}
get h() {return this._height;}

get active() { return this._active;}
Activate() { this._active = true;}
Deactivate() { this._active = false;}
}

在点击判断中,首先要判断点击是否在我们对象的区域内:

1
2
3
4
5
6
/**  @returns {boolean} */
IsMouseOver() {
//通过PIXI的原生函数 DisplayObject.worldTransform.applyInverse(p: Point) 可以方便地把全局坐标变成本地坐标
const pos = this.worldTransform.applyInverse(new Point(TouchInput.x, TouchInput.y));
return pos.x >= 0 && pos.x <= this.w && pos.y >= 0 && pos.y <= this.h;
}

接写来需要处理点击事件, 按照RM的风格我们把这个放到 update函数中。判定点击需要考虑以下情况:

  • 鼠标按下 -> OnPress
  • 鼠标按下后立即原地释放 -> OnRelease & OnClick
  • 鼠标按下一段时间后原地释放 -> OnRelease & OnLongPressRelease (长按后即使原地释放一般也不认为是点击,考虑到长按一般是弹出悬浮窗口显示详情,这是释放应该是窗口消失,直接触发点击并不适宜。)
  • 鼠标按下后移动到区域内另一位置释放 -> OnRelease & OnLongPressRelease
  • 鼠标按下后移动到区域之外 -> OnLongPressRelease (鼠标处于点击状态移动到区域之外,如果有长按悬浮窗这时也应该消失,所以也处理为长按释放)

整理之后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
_pressed = false; //记录“鼠标在区域内按下”
_longPressed = false; //记录“鼠标在区域内长按”
_pressPoint = new Point(0, 0); //记录鼠标按下的点坐标
_releasePoint = new Point(0, 0); //记录鼠标释放的点坐标

IsEnabled() {
return this.visible && this._active;
}

update() {
if (!this.IsEnabled()) return;

for (const child of this.children.reverse.clone()) {
if (child.update) {
child.update();
}
}

if (this._pressed) { //已经处于按下状态
if (this.IsMouseOver()) {
if (!this._longPressed && TouchInput.isLongPressed()) {
this._longPressed = true;
this.OnLongPress();
}
if(TouchInput.isReleased()) {
if(this._longPressed) {
this.OnLongPressRelease(true);
} else if(TouchInput.isClicked()){
this.OnClick();
}
this._pressed = false;
this._longPressed = false;
this.OnRelease();
}
} else { //鼠标在按下状态离开区域的情况
if(this._longPressed) {
this.OnLongPressRelease(false);
}
this._pressed = false;
this._longPressed = false;
this.OnRelease();
}
} else if (TouchInput.isTriggered()&&this.IsMouseOver()) { //开始按下
this._pressed = true;
this.OnPress();
}
}

接下来是几个触发事件的函数,为了方便测试这里先用几个 console.log 好查看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
_handlers = {};

/**
* @param {Function} func
* @param {Function} handle_func
* @param args
*/
SetHandler(func, handle_func, ...args) {
if(!this[func.name]) {
throw new Error(`No method named ${func.name} found!`);
} else {
this._handlers[func.name] = handle_func.bind(this, ...args);
}
}

CallHandler(name) {
if(this._handlers[name]) {
this._handlers[name]();
}
}
s
OnPress() {
this._pressPoint.set(TouchInput.x, TouchInput.y);
console.log('press');
this.CallHandler(this.OnPress.name);
}

OnRelease() {
this._releasePoint.set(TouchInput.x, TouchInput.y);
console.log('release');
this.CallHandler(this.OnRelease.name);
}

OnClick() {
console.log('click');
this.CallHandler(this.OnClick.name);
}

OnLongPress() {
console.log('long press start');
this.CallHandler(this.OnLongPress.name);
}

/**
* @param {boolean} isClick
*/
OnLongPressRelease(isClick) {
console.log(`long press release, this is${isClick?'':' not'} a click`);
this.CallHandler(this.OnLongPressRelease.name);
}