refactor: MotaOffscreenCanvas2D 的垃圾回收机制

This commit is contained in:
unanmed 2025-02-21 16:11:57 +08:00
parent e79c530c48
commit ac5eefdb02
11 changed files with 320 additions and 61 deletions

View File

@ -3149,7 +3149,7 @@ control.prototype.resize = function () {
core.domStyle.scale = target - 0.25;
}
const pw = (480 + 180) * core.domStyle.scale;
const pw = (480 + 180 * 2) * core.domStyle.scale;
const ph = 480 * core.domStyle.scale;
core.dom.gameDraw.style.width = `${pw}px`;
core.dom.gameDraw.style.height = `${ph}px`;

View File

@ -33,8 +33,14 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
return this._freezed;
}
private _active: boolean = true;
get active() {
return this._active;
}
/**
*
* \
* ****使使使 `RenderItem.requireCanvas`
* @param alpha
* @param canvas
*/
@ -146,6 +152,27 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
MotaOffscreenCanvas2D.list.delete(this);
}
/**
* 使使
*/
activate() {
if (this._active || this._freezed) return;
MotaOffscreenCanvas2D.list.add(this);
if (this.autoScale) {
this.size(this.width, this.height);
this.symbol++;
this.emit('resize');
}
}
/**
* 使使
*/
deactivate() {
if (!this._active || this._freezed) return;
MotaOffscreenCanvas2D.list.delete(this);
}
/**
* Canvas2D对象
* @param canvas MotaOffscreenCanvas2D对象

View File

@ -316,13 +316,15 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
//#region 渲染配置与缓存
/** 渲染缓存信息 */
protected cache: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D();
protected cache: MotaOffscreenCanvas2D;
/** 是否需要更新缓存 */
protected cacheDirty: boolean = false;
/** 是否启用缓存机制 */
readonly enableCache: boolean = true;
/** 是否启用transform下穿机制即画布的变换是否会继续作用到下一层画布 */
readonly transformFallThrough: boolean = false;
/** 这个渲染元素使用到的所有画布 */
protected readonly canvases: Set<MotaOffscreenCanvas2D> = new Set();
//#endregion
//#region 交互事件
@ -356,6 +358,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.type = type;
this._transform.bind(this);
this.cache = this.requireCanvas();
this.cache.withGameScale(true);
}
@ -414,6 +417,16 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.emit('afterRender', transform);
}
/**
* `MotaOffscreenCanvas2D`
* @param alpha alpha
*/
protected requireCanvas(alpha: boolean = true) {
const canvas = new MotaOffscreenCanvas2D(alpha);
this.canvases.add(canvas);
return canvas;
}
//#region 修改元素属性
/**
@ -625,6 +638,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.update();
this.checkRoot();
this._root?.connect(this);
this.canvases.forEach(v => v.activate());
}
/**
@ -638,6 +652,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this._parent = void 0;
parent.requestSort();
parent.update();
this.canvases.forEach(v => v.deactivate());
if (!success) return false;
this._root?.disconnect(this);
this._root = void 0;

View File

@ -498,8 +498,7 @@ export class Damage extends RenderItem<EDamageEvent> {
this.emit('dirtyUpdate', v);
// 否则依次渲染并写入缓存
const temp =
block.cache.get(v)?.canvas ?? new MotaOffscreenCanvas2D();
const temp = block.cache.get(v)?.canvas ?? this.requireCanvas();
temp.clear();
temp.setHD(true);
temp.setAntiAliasing(true);

View File

@ -10,7 +10,6 @@ import { Transform } from '../transform';
import { LayerFloorBinder, LayerGroupFloorBinder } from './floor';
import { RenderAdapter } from '../adapter';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { Camera } from '../camera';
import { IAnimateFrame, renderEmits } from '../frame';
export interface ILayerGroupRenderExtends {
@ -358,7 +357,7 @@ export class LayerGroup
return;
}
case 'camera':
if (!this.assertType(nextValue, Camera, key)) return;
if (!this.assertType(nextValue, Transform, key)) return;
this.camera = nextValue;
return;
}
@ -571,11 +570,11 @@ export class Layer extends Container<ELayerEvent> {
static readonly FRAME_ALL = 15;
/** 静态层,包含除大怪物及正在移动的内容外的内容 */
protected staticMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D();
protected staticMap: MotaOffscreenCanvas2D = this.requireCanvas();
/** 移动层,包含大怪物及正在移动的内容 */
protected movingMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D();
protected movingMap: MotaOffscreenCanvas2D = this.requireCanvas();
/** 背景图层 */
protected backMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D();
protected backMap: MotaOffscreenCanvas2D = this.requireCanvas();
/** 最终渲染至的Sprite */
main: Sprite = new Sprite('absolute', false, true);
@ -783,12 +782,12 @@ export class Layer extends Container<ELayerEvent> {
if (!data) return;
const frame = data.frame;
const temp = new MotaOffscreenCanvas2D();
const temp = this.requireCanvas();
temp.setHD(false);
temp.setAntiAliasing(false);
temp.withGameScale(false);
for (let i = 0; i < frame; i++) {
const canvas = new MotaOffscreenCanvas2D();
const canvas = this.requireCanvas();
const ctx = canvas.ctx;
const tempCtx = temp.ctx;
const [sx, sy, w, h] = data.render[i];
@ -1206,7 +1205,7 @@ export class Layer extends Container<ELayerEvent> {
const ex = Math.min(sx + blockSize, this.mapWidth);
const ey = Math.min(sy + blockSize, this.mapHeight);
const temp = new MotaOffscreenCanvas2D();
const temp = this.requireCanvas();
temp.setAntiAliasing(false);
temp.setHD(false);
temp.withGameScale(false);
@ -1478,7 +1477,7 @@ export class Layer extends Container<ELayerEvent> {
return;
case 'floorImage':
if (!this.assertType(nextValue, Array, key)) return;
this.setFloorImage(nextValue);
this.setFloorImage(nextValue as FloorAnimate[]);
return;
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);

View File

@ -19,7 +19,7 @@ export class Text extends RenderItem<ETextEvent> {
fillStyle?: CanvasStyle = '#fff';
strokeStyle?: CanvasStyle;
font?: string = '';
font: string = '16px Verdana';
strokeWidth: number = 1;
private length: number = 0;
@ -42,7 +42,7 @@ export class Text extends RenderItem<ETextEvent> {
ctx.textBaseline = 'bottom';
ctx.fillStyle = this.fillStyle ?? 'transparent';
ctx.strokeStyle = this.strokeStyle ?? 'transparent';
ctx.font = this.font ?? '';
ctx.font = this.font;
ctx.lineWidth = this.strokeWidth;
if (this.strokeStyle) {
@ -59,7 +59,7 @@ export class Text extends RenderItem<ETextEvent> {
measure() {
const ctx = Text.measureCanvas.ctx;
ctx.textBaseline = 'bottom';
ctx.font = this.font ?? '';
ctx.font = this.font;
const res = ctx.measureText(this.text);
return res;
}

View File

@ -0,0 +1,42 @@
import { ElementLocator, Sprite } from '@/core/render';
import { defineComponent, onMounted, ref, watch } from 'vue';
import { SetupComponentOptions } from './types';
interface ProgressProps {
/** 进度条的位置 */
loc: ElementLocator;
/** 进度条的进度1表示完成0表示未完成 */
progress: number;
/** 已完成部分的样式默认为绿色green */
success?: CanvasStyle;
/** 未完成部分的样式默认为灰色gray */
background?: CanvasStyle;
}
const progressProps = {
props: ['loc', 'progress', 'success', 'background']
} satisfies SetupComponentOptions<ProgressProps>;
export const Progress = defineComponent<ProgressProps>(props => {
const element = ref<Sprite>();
onMounted(() => {
element.value?.setRenderFn(canvas => {
const { ctx } = canvas;
const width = props.loc[2] ?? 200;
const height = props.loc[3] ?? 200;
ctx.fillStyle = props.background ?? 'gray';
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = props.success ?? 'green';
ctx.fillRect(0, 0, width * props.progress, height);
});
});
watch(props, () => {
element.value?.update();
});
return () => {
return <sprite ref={element} loc={props.loc}></sprite>;
};
}, progressProps);

View File

@ -6,5 +6,5 @@ export const ENABLE_RIGHT_STATUS_BAR = true;
export const MAP_WIDTH = 480;
export const MAP_HEIGHT = 480;
export const MAIN_WIDTH = 480 + 180;
export const MAIN_WIDTH = 480 + 180 * 2;
export const MAIN_HEIGHT = 480;

View File

@ -26,7 +26,12 @@ import {
STATUS_BAR_HEIGHT,
STATUS_BAR_WIDTH
} from '../shared';
import { IHeroStatus, StatusBar } from './statusBar';
import {
ILeftHeroStatus,
IRightHeroStatus,
LeftStatusBar,
RightStatusBar
} from './statusBar';
import { onLoaded } from '../use';
const MainScene = defineComponent(() => {
@ -71,25 +76,58 @@ const MainScene = defineComponent(() => {
weather.bind(map.value);
});
const status: IHeroStatus = reactive({
const leftStatus: ILeftHeroStatus = reactive({
hp: 0,
atk: 0,
def: 0,
mdef: 0
mdef: 0,
money: 0,
exp: 0,
yellowKey: 0,
blueKey: 0,
redKey: 0,
floor: 'MT0',
lv: '',
regen: 0,
exAtk: 0,
magicDef: 0
});
const rightStatus: IRightHeroStatus = reactive({});
const { getHeroStatusOn } = Mota.requireAll('fn');
const updateStatus = () => {
const hero = core.status.hero;
leftStatus.atk = getHeroStatusOn('atk');
leftStatus.hp = getHeroStatusOn('hp');
leftStatus.def = getHeroStatusOn('def');
leftStatus.mdef = getHeroStatusOn('mdef');
leftStatus.money = getHeroStatusOn('money');
leftStatus.exp = core.getNextLvUpNeed() ?? 0;
leftStatus.yellowKey = core.itemCount('yellowKey');
leftStatus.blueKey = core.itemCount('blueKey');
leftStatus.redKey = core.itemCount('redKey');
leftStatus.floor = core.status.floorId;
leftStatus.lv = core.getLvName(hero.lv);
leftStatus.regen = getHeroStatusOn('hpmax');
leftStatus.exAtk = getHeroStatusOn('mana');
leftStatus.magicDef = getHeroStatusOn('magicDef');
};
const loaded = ref(false);
onLoaded(() => {
loaded.value = true;
});
Mota.require('var', 'hook').on('statusBarUpdate', updateStatus);
return () => (
<container id="main-scene" width={MAIN_WIDTH} height={MAIN_HEIGHT}>
{loaded.value && (
<StatusBar
<LeftStatusBar
loc={[0, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
status={status}
></StatusBar>
status={leftStatus}
></LeftStatusBar>
)}
<container id="map-draw" {...mapDrawProps} x={180} zIndex={10}>
<layer-group id="layer-main" ex={layerGroupExtends} ref={map}>
@ -103,6 +141,12 @@ const MainScene = defineComponent(() => {
<Textbox id="main-textbox" {...mainTextboxProps}></Textbox>
<FloorChange id="floor-change" zIndex={50}></FloorChange>
</container>
{loaded.value && (
<RightStatusBar
loc={[480 + 180, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
status={rightStatus}
></RightStatusBar>
)}
{mainUIController.render()}
</container>
);

View File

@ -3,54 +3,181 @@ import { defineComponent } from 'vue';
import { SetupComponentOptions } from '../components';
import { ElementLocator } from '@/core/render';
export interface IHeroStatus {
export interface ILeftHeroStatus {
hp: number;
atk: number;
def: number;
mdef: number;
money: number;
exp: number;
yellowKey: number;
blueKey: number;
redKey: number;
floor: FloorIds;
lv: string;
/** 生命回复 */
regen: number;
/** 额外攻击 */
exAtk: number;
/** 魔法防御 */
magicDef: number;
}
interface StatusBarProps {
export interface IRightHeroStatus {}
interface StatusBarProps<T> {
loc: ElementLocator;
status: IHeroStatus;
status: T;
}
const statusBarProps = {
props: ['loc', 'status']
} satisfies SetupComponentOptions<StatusBarProps>;
} satisfies SetupComponentOptions<StatusBarProps<unknown>>;
export const StatusBar = defineComponent<StatusBarProps>(p => {
const hpIcon = core.material.images.images['hp.png'];
const atkIcon = core.material.images.images['atk.png'];
const defIcon = core.material.images.images['def.png'];
const mdefIcon = core.material.images.images['IQ.png'];
export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
p => {
const hpIcon = core.material.images.images['hp.png'];
const atkIcon = core.material.images.images['atk.png'];
const defIcon = core.material.images.images['def.png'];
const mdefIcon = core.material.images.images['IQ.png'];
const moneyIcon = core.material.images.images['money.png'];
const expIcon = core.material.images.images['exp.png'];
const s = p.status;
const f = core.formatBigNumber;
const s = p.status;
const f = core.formatBigNumber;
const iconLoc = (n: number): ElementLocator => {
return [16, 16 + 48 * n, 32, 32];
};
const floorName = core.floors[s.floor].title;
const textLoc = (n: number): ElementLocator => {
return [64, 32 + 48 * n, void 0, void 0, 0.5, 0.5];
};
const key = (num: number) => {
return num.toString().padStart(2, '0');
};
return () => {
return (
<container loc={p.loc}>
<g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect>
<image image={hpIcon} loc={iconLoc(0)}></image>
<text text={f(s.hp)} loc={textLoc(0)}></text>
<image image={atkIcon} loc={iconLoc(1)}></image>
<text text={f(s.atk)} loc={textLoc(1)}></text>
<image image={defIcon} loc={iconLoc(2)}></image>
<text text={f(s.atk)} loc={textLoc(2)}></text>
<image image={mdefIcon} loc={iconLoc(3)}></image>
<text text={f(s.atk)} loc={textLoc(3)}></text>
</container>
);
};
}, statusBarProps);
const font1 = '18px normal';
const font2 = 'bold 18px normal';
const font3 = 'bold 14px normal';
export const statusBarUI = new GameUI('status-bar', StatusBar);
const iconLoc = (n: number): ElementLocator => {
return [16, 76 + 44 * n, 32, 32];
};
const textLoc = (n: number): ElementLocator => {
return [60, 92 + 44 * n, void 0, void 0, 0, 0.5];
};
const central = (y: number): ElementLocator => {
const width = p.loc[2] ?? 200;
return [width / 2, y, void 0, void 0, 0.5, 0.5];
};
const right = (y: number): ElementLocator => {
const width = p.loc[2] ?? 200;
return [width - 16, y, void 0, void 0, 1, 0.5];
};
const keyCount = 3;
const keyY = 92 + 44 * 6;
const keyLoc = (n: number): ElementLocator => {
const width = p.loc[2] ?? 200;
const per = width / (keyCount + 1);
return [per * (n + 1), keyY, void 0, void 0, 0.5, 0.5];
};
return () => {
return (
<container loc={p.loc}>
<g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect>
<text
text={floorName}
loc={central(24)}
font={font1}
cursor="pointer"
></text>
<text text={s.lv} loc={central(54)} font={font1}></text>
<image image={hpIcon} loc={iconLoc(0)}></image>
<text text={f(s.hp)} loc={textLoc(0)} font={font1}></text>
<text
text={`+${f(s.regen)}/t`}
loc={right(110)}
font={font3}
fillStyle="#a7ffa7"
></text>
<image image={atkIcon} loc={iconLoc(1)}></image>
<text text={f(s.atk)} loc={textLoc(1)} font={font1}></text>
<text
text={`+${f(s.exAtk)}`}
loc={right(154)}
font={font3}
fillStyle="#ffd3d3"
></text>
<image image={defIcon} loc={iconLoc(2)}></image>
<text text={f(s.def)} loc={textLoc(2)} font={font1}></text>
{s.magicDef > 0 && (
<text
text={`+${f(s.magicDef)}`}
loc={right(198)}
font={font3}
fillStyle="#b0bdff"
></text>
)}
<image image={mdefIcon} loc={iconLoc(3)}></image>
<text text={f(s.mdef)} loc={textLoc(3)} font={font1}></text>
<image image={moneyIcon} loc={iconLoc(4)}></image>
<text
text={f(s.money)}
loc={textLoc(4)}
font={font1}
></text>
<image image={expIcon} loc={iconLoc(5)}></image>
<text text={f(s.exp)} loc={textLoc(5)} font={font1}></text>
<text
text={key(s.yellowKey)}
loc={keyLoc(0)}
font={font2}
fillStyle="#fca"
></text>
<text
text={key(s.blueKey)}
loc={keyLoc(1)}
font={font2}
fillStyle="#aad"
></text>
<text
text={key(s.redKey)}
loc={keyLoc(2)}
font={font2}
fillStyle="#f88"
></text>
<text
text="技能树"
loc={central(396)}
font={font1}
cursor="pointer"
></text>
<text
text="查看技能"
loc={central(428)}
font={font1}
cursor="pointer"
></text>
</container>
);
};
},
statusBarProps
);
export const RightStatusBar = defineComponent<StatusBarProps<IRightHeroStatus>>(
p => {
return () => {
return (
<container loc={p.loc}>
<g-rect loc={[0, 0, p.loc[2], p.loc[3]]} stroke></g-rect>
</container>
);
};
},
statusBarProps
);
export const leftStatusBarUI = new GameUI('left-status-bar', LeftStatusBar);
export const rightStatusBarUI = new GameUI('right-status-bar', RightStatusBar);

View File

@ -262,7 +262,9 @@ onUnmounted(() => {
font-size: 200%;
width: 100%;
margin-bottom: 14px;
text-shadow: 3px 2px 3px #000, 0px 0px 3px #111;
text-shadow:
3px 2px 3px #000,
0px 0px 3px #111;
display: flex;
flex-direction: row;
align-items: center;
@ -303,7 +305,9 @@ onUnmounted(() => {
font-size: 200%;
width: 100%;
text-align: center;
text-shadow: 3px 2px 3px #000, 0px 0px 3px #111;
text-shadow:
3px 2px 3px #000,
0px 0px 3px #111;
}
#status-lv {
@ -311,7 +315,9 @@ onUnmounted(() => {
font-size: 200%;
width: 100%;
text-align: center;
text-shadow: 3px 2px 3px #000, 0px 0px 3px #111;
text-shadow:
3px 2px 3px #000,
0px 0px 3px #111;
}
.status-extra {