mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-31 23:29:27 +08:00
feat: 动画
This commit is contained in:
parent
63765ab626
commit
d0ff2f1dc5
@ -1,5 +1,6 @@
|
||||
type AdapterFunction<T> = (item: T, ...params: any[]) => Promise<any>;
|
||||
type SyncAdapterFunction<T> = (item: T, ...params: any[]) => any;
|
||||
type GlobalAdapterFunction = (...params: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 渲染适配器,用作渲染层与数据层沟通的桥梁,用于在数据层等待渲染层执行,常用与动画等。
|
||||
@ -16,6 +17,7 @@ export class RenderAdapter<T> {
|
||||
|
||||
private execute: Map<string, AdapterFunction<T>> = new Map();
|
||||
private syncExecutes: Map<string, SyncAdapterFunction<T>> = new Map();
|
||||
private globalExecutes: Map<string, GlobalAdapterFunction> = new Map();
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
@ -36,11 +38,20 @@ export class RenderAdapter<T> {
|
||||
this.items.delete(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义一个类似于静态函数的函数,只会调用一次,不会对每个元素执行
|
||||
* @param id 函数名称
|
||||
* @param fn 执行的函数
|
||||
*/
|
||||
receiveGlobal(id: string, fn: GlobalAdapterFunction) {
|
||||
this.globalExecutes.set(id, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置执行函数
|
||||
* @param fn 对于每个元素执行的函数
|
||||
*/
|
||||
recieve(id: string, fn: AdapterFunction<T>): void {
|
||||
receive(id: string, fn: AdapterFunction<T>): void {
|
||||
this.execute.set(id, fn);
|
||||
}
|
||||
|
||||
@ -48,7 +59,7 @@ export class RenderAdapter<T> {
|
||||
* 设置同步执行函数
|
||||
* @param fn 对于每个元素执行的函数
|
||||
*/
|
||||
recieveSync(id: string, fn: SyncAdapterFunction<T>): void {
|
||||
receiveSync(id: string, fn: SyncAdapterFunction<T>): void {
|
||||
this.syncExecutes.set(id, fn);
|
||||
}
|
||||
|
||||
@ -96,6 +107,18 @@ export class RenderAdapter<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用一个全局函数
|
||||
*/
|
||||
global<R = any>(id: string, ...params: any[]): Promise<R> {
|
||||
const execute = this.globalExecutes.get(id);
|
||||
if (!execute) {
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return execute(...params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁这个adapter
|
||||
*/
|
||||
|
@ -6,6 +6,7 @@ import { LayerGroup, FloorLayer } from './preset/layer';
|
||||
import { MotaRenderer } from './render';
|
||||
import { LayerShadowExtends } from '../fx/shadow';
|
||||
import { LayerGroupFilter } from '@/plugin/fx/gameCanvas';
|
||||
import { LayerGroupAnimate } from './preset/animate';
|
||||
|
||||
let main: MotaRenderer;
|
||||
|
||||
@ -31,12 +32,14 @@ Mota.require('var', 'loading').once('loaded', () => {
|
||||
const door = new LayerDoorAnimate();
|
||||
const shadow = new LayerShadowExtends();
|
||||
const filter = new LayerGroupFilter();
|
||||
const animate = new LayerGroupAnimate();
|
||||
layer.extends(damage);
|
||||
layer.extends(detail);
|
||||
layer.extends(filter);
|
||||
layer.getLayer('event')?.extends(hero);
|
||||
layer.getLayer('event')?.extends(door);
|
||||
layer.getLayer('event')?.extends(shadow);
|
||||
layer.extends(animate);
|
||||
|
||||
render.appendChild(layer);
|
||||
});
|
||||
|
262
src/core/render/preset/animate.ts
Normal file
262
src/core/render/preset/animate.ts
Normal file
@ -0,0 +1,262 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { RenderAdapter } from '../adapter';
|
||||
import { Sprite } from '../sprite';
|
||||
import { HeroRenderer } from './hero';
|
||||
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { transformCanvas } from '../item';
|
||||
|
||||
export class LayerGroupAnimate implements ILayerGroupRenderExtends {
|
||||
static animateList: Set<LayerGroupAnimate> = new Set();
|
||||
id: string = 'animate';
|
||||
|
||||
group!: LayerGroup;
|
||||
hero!: HeroRenderer;
|
||||
animate!: Animate;
|
||||
|
||||
private animation: Set<AnimateData> = new Set();
|
||||
|
||||
/**
|
||||
* 绘制一个跟随勇士的动画
|
||||
* @param name 动画id
|
||||
*/
|
||||
async drawHeroAnimate(name: AnimationIds) {
|
||||
const animate = this.animate.animate(name, 0, 0);
|
||||
this.updatePosition(animate);
|
||||
await this.animate.draw(animate);
|
||||
this.animation.delete(animate);
|
||||
}
|
||||
|
||||
private updatePosition(animate: AnimateData) {
|
||||
if (!this.hero.renderable) return;
|
||||
const { x, y } = this.hero.renderable;
|
||||
const cell = this.group.cellSize;
|
||||
const half = cell / 2;
|
||||
animate.centerX = x * cell + half;
|
||||
animate.centerY = y * cell + half;
|
||||
}
|
||||
|
||||
private onMoveTick = (x: number, y: number) => {
|
||||
const cell = this.group.cellSize;
|
||||
const half = cell / 2;
|
||||
const ax = x * cell + half;
|
||||
const ay = y * cell + half;
|
||||
this.animation.forEach(v => {
|
||||
v.centerX = ax;
|
||||
v.centerY = ay;
|
||||
});
|
||||
};
|
||||
|
||||
private listen() {
|
||||
this.hero.on('moveTick', this.onMoveTick);
|
||||
}
|
||||
|
||||
awake(group: LayerGroup): void {
|
||||
this.group = group;
|
||||
const ex = group.getLayer('event')?.getExtends('floor-hero');
|
||||
if (ex instanceof HeroRenderer) {
|
||||
this.hero = ex;
|
||||
this.animate = new Animate();
|
||||
this.animate.size(group.width, group.height);
|
||||
this.animate.setHD(true);
|
||||
this.animate.setZIndex(100);
|
||||
group.appendChild(this.animate);
|
||||
LayerGroupAnimate.animateList.add(this);
|
||||
this.listen();
|
||||
} else {
|
||||
logger.error(
|
||||
14,
|
||||
`Animate extends needs 'floor-hero' extends as dependency.`
|
||||
);
|
||||
group.removeExtends('animate');
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(group: LayerGroup): void {
|
||||
this.hero.off('moveTick', this.onMoveTick);
|
||||
LayerGroupAnimate.animateList.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
interface AnimateData {
|
||||
obj: globalThis.Animate;
|
||||
/** 第一帧是全局第几帧 */
|
||||
readonly start: number;
|
||||
/** 当前是第几帧 */
|
||||
index: number;
|
||||
/** 是否需要播放音频 */
|
||||
sound: boolean;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
onEnd?: () => void;
|
||||
readonly absolute: boolean;
|
||||
}
|
||||
|
||||
export class Animate extends Sprite {
|
||||
/** 绝对位置的动画 */
|
||||
private absoluteAnimates: Set<AnimateData> = new Set();
|
||||
/** 静态位置的动画 */
|
||||
private staticAnimates: Set<AnimateData> = new Set();
|
||||
|
||||
private delegation: number;
|
||||
private frame: number = 0;
|
||||
private lastTime: number = 0;
|
||||
|
||||
constructor() {
|
||||
super('absolute', false);
|
||||
|
||||
this.setRenderFn((canvas, transform) => {
|
||||
const { ctx } = canvas;
|
||||
ctx.save();
|
||||
this.drawAnimates(this.absoluteAnimates, canvas);
|
||||
transformCanvas(canvas, transform);
|
||||
this.drawAnimates(this.staticAnimates, canvas);
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
this.delegation = this.delegateTicker(time => {
|
||||
if (time - this.lastTime < 50) return;
|
||||
this.lastTime = time;
|
||||
this.frame++;
|
||||
if (
|
||||
this.absoluteAnimates.size > 0 ||
|
||||
this.staticAnimates.size > 0
|
||||
) {
|
||||
this.update(this);
|
||||
}
|
||||
});
|
||||
|
||||
adapter.add(this);
|
||||
}
|
||||
|
||||
private drawAnimates(
|
||||
data: Set<AnimateData>,
|
||||
canvas: MotaOffscreenCanvas2D
|
||||
) {
|
||||
if (data.size === 0) return;
|
||||
const { ctx } = canvas;
|
||||
const toDelete = new Set<AnimateData>();
|
||||
data.forEach(v => {
|
||||
const obj = v.obj;
|
||||
const index = v.index;
|
||||
const frame = obj.frames[index];
|
||||
const ratio = obj.ratio;
|
||||
if (!v.sound) {
|
||||
const se = (index % obj.frame) + 1;
|
||||
core.playSound(v.obj.se[se], v.obj.pitch[se]);
|
||||
v.sound = true;
|
||||
}
|
||||
const centerX = v.centerX;
|
||||
const centerY = v.centerY;
|
||||
|
||||
frame.forEach(v => {
|
||||
const img = obj.images[v.index];
|
||||
if (!img) return;
|
||||
|
||||
const realWidth = (img.width * ratio * v.zoom) / 100;
|
||||
const realHeight = (img.height * ratio * v.zoom) / 100;
|
||||
ctx.globalAlpha = v.opacity / 255;
|
||||
|
||||
const cx = centerX + v.x;
|
||||
const cy = centerY + v.y;
|
||||
|
||||
const ix = -realWidth / 2;
|
||||
const iy = -realHeight / 2;
|
||||
const angle = v.angle ? (-v.angle * Math.PI) / 180 : 0;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
if (v.mirror) {
|
||||
ctx.scale(-1, 1);
|
||||
}
|
||||
ctx.rotate(angle);
|
||||
ctx.drawImage(img, ix, iy, realWidth, realHeight);
|
||||
ctx.restore();
|
||||
});
|
||||
const now = this.frame - v.start;
|
||||
if (now !== v.index) v.sound = true;
|
||||
v.index = now;
|
||||
if (v.index === v.obj.frame) {
|
||||
toDelete.add(v);
|
||||
}
|
||||
});
|
||||
toDelete.forEach(v => {
|
||||
data.delete(v);
|
||||
v.onEnd?.();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个可以被执行的动画
|
||||
* @param name 动画名称
|
||||
* @param absolute 是否是绝对定位,绝对定位不会受到transform的影响
|
||||
*/
|
||||
animate(
|
||||
name: AnimationIds,
|
||||
x: number,
|
||||
y: number,
|
||||
absolute: boolean = false
|
||||
) {
|
||||
const animate = core.material.animates[name];
|
||||
const data: AnimateData = {
|
||||
index: 0,
|
||||
start: this.frame,
|
||||
obj: animate,
|
||||
centerX: x,
|
||||
centerY: y,
|
||||
absolute,
|
||||
sound: false
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制动画,动画结束时兑现返回的Promise
|
||||
* @param animate 动画信息
|
||||
* @returns
|
||||
*/
|
||||
draw(animate: AnimateData): Promise<void> {
|
||||
return new Promise(res => {
|
||||
if (animate.absolute) {
|
||||
this.absoluteAnimates.add(animate);
|
||||
} else {
|
||||
this.staticAnimates.add(animate);
|
||||
}
|
||||
animate.onEnd = () => {
|
||||
res();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据动画名称、坐标、定位绘制动画
|
||||
* @param name 动画名称
|
||||
* @param absolute 是否是绝对定位
|
||||
*/
|
||||
drawAnimate(
|
||||
name: AnimationIds,
|
||||
x: number,
|
||||
y: number,
|
||||
absolute: boolean = false
|
||||
) {
|
||||
return this.draw(this.animate(name, x, y, absolute));
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
this.removeTicker(this.delegation);
|
||||
adapter.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = new RenderAdapter<Animate>('animate');
|
||||
adapter.receive('drawAnimate', (item, name, x, y, absolute) => {
|
||||
return item.drawAnimate(name, x, y, absolute);
|
||||
});
|
||||
adapter.receiveGlobal('drawHeroAnimate', name => {
|
||||
const execute: Promise<void>[] = [];
|
||||
LayerGroupAnimate.animateList.forEach(v => {
|
||||
execute.push(v.drawHeroAnimate(name));
|
||||
});
|
||||
return Promise.all(execute);
|
||||
});
|
@ -381,9 +381,9 @@ export class LayerDoorAnimate implements ILayerRenderExtends {
|
||||
}
|
||||
|
||||
const doorAdapter = new RenderAdapter<LayerDoorAnimate>('door-animate');
|
||||
doorAdapter.recieve('openDoor', (item, block: Block) => {
|
||||
doorAdapter.receive('openDoor', (item, block: Block) => {
|
||||
return item.openDoor(block);
|
||||
});
|
||||
doorAdapter.recieve('closeDoor', (item, block: Block) => {
|
||||
doorAdapter.receive('closeDoor', (item, block: Block) => {
|
||||
return item.closeDoor(block);
|
||||
});
|
||||
|
@ -359,42 +359,42 @@ export class HeroRenderer
|
||||
}
|
||||
|
||||
const adapter = new RenderAdapter<HeroRenderer>('hero-adapter');
|
||||
adapter.recieve('readyMove', item => {
|
||||
adapter.receive('readyMove', item => {
|
||||
item.readyMove();
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.recieve('move', (item, dir: Dir) => {
|
||||
adapter.receive('move', (item, dir: Dir) => {
|
||||
return item.move(dir);
|
||||
});
|
||||
adapter.recieve('endMove', item => {
|
||||
adapter.receive('endMove', item => {
|
||||
return item.endMove();
|
||||
});
|
||||
adapter.recieve(
|
||||
adapter.receive(
|
||||
'moveAs',
|
||||
(item, x: number, y: number, time: number, fn: TimingFn<3>) => {
|
||||
return item.moveAs(x, y, time, fn);
|
||||
}
|
||||
);
|
||||
adapter.recieve('setMoveSpeed', (item, speed: number) => {
|
||||
adapter.receive('setMoveSpeed', (item, speed: number) => {
|
||||
item.setMoveSpeed(speed);
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.recieve('setHeroLoc', (item, x?: number, y?: number) => {
|
||||
adapter.receive('setHeroLoc', (item, x?: number, y?: number) => {
|
||||
item.setHeroLoc(x, y);
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.recieve('turn', (item, dir: Dir2) => {
|
||||
adapter.receive('turn', (item, dir: Dir2) => {
|
||||
item.turn(dir);
|
||||
return Promise.resolve();
|
||||
});
|
||||
adapter.recieve('setImage', (item, image: SizedCanvasImageSource) => {
|
||||
adapter.receive('setImage', (item, image: SizedCanvasImageSource) => {
|
||||
item.setImage(image);
|
||||
return Promise.resolve();
|
||||
});
|
||||
// 同步fallback,用于适配现在的样板,之后会删除
|
||||
adapter.recieveSync('setHeroLoc', (item, x?: number, y?: number) => {
|
||||
adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => {
|
||||
item.setHeroLoc(x, y);
|
||||
});
|
||||
adapter.recieveSync('turn', (item, dir: Dir2) => {
|
||||
adapter.receiveSync('turn', (item, dir: Dir2) => {
|
||||
item.turn(dir);
|
||||
});
|
||||
|
@ -1,26 +1,30 @@
|
||||
import type { RenderAdapter } from '@/core/render/adapter';
|
||||
import type { LayerGroupAnimate } from '@/core/render/preset/animate';
|
||||
import type { LayerDoorAnimate } from '@/core/render/preset/floor';
|
||||
import type { HeroRenderer } from '@/core/render/preset/hero';
|
||||
import { hook } from '@/game/game';
|
||||
|
||||
interface Adapters {
|
||||
'hero-adapter'?: RenderAdapter<HeroRenderer>;
|
||||
'door-animate'?: RenderAdapter<LayerDoorAnimate>;
|
||||
animate?: RenderAdapter<LayerGroupAnimate>;
|
||||
}
|
||||
|
||||
const adapters: Adapters = {};
|
||||
|
||||
export function init() {
|
||||
const hook = Mota.require('var', 'hook');
|
||||
const loading = Mota.require('var', 'loading');
|
||||
let fallbackIds: number = 1e8;
|
||||
|
||||
if (!main.replayChecking && main.mode === 'play') {
|
||||
const Adapter = Mota.require('module', 'Render').RenderAdapter;
|
||||
const hero = Adapter.get<HeroRenderer>('hero-adapter');
|
||||
const doorAnimate = Adapter.get<LayerDoorAnimate>('door-animate');
|
||||
const animate = Adapter.get<LayerGroupAnimate>('animate');
|
||||
|
||||
adapters['hero-adapter'] = hero;
|
||||
adapters['door-animate'] = doorAnimate;
|
||||
adapters['animate'] = animate;
|
||||
}
|
||||
|
||||
let moving: boolean = false;
|
||||
@ -391,6 +395,87 @@ export function init() {
|
||||
this._openDoor_animate(block, x, y, callback);
|
||||
}
|
||||
};
|
||||
|
||||
// ----- animate & hero animate
|
||||
////// 绘制动画 //////
|
||||
maps.prototype.drawAnimate = function (
|
||||
name: AnimationIds,
|
||||
x: number,
|
||||
y: number,
|
||||
alignWindow?: boolean,
|
||||
callback?: () => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
name = core.getMappedName(name);
|
||||
|
||||
// 正在播放录像:不显示动画
|
||||
if (
|
||||
core.isReplaying() ||
|
||||
!core.material.animates[name] ||
|
||||
x == null ||
|
||||
y == null
|
||||
) {
|
||||
if (callback) callback();
|
||||
return -1;
|
||||
}
|
||||
|
||||
adapters.animate
|
||||
?.all(
|
||||
'drawAnimate',
|
||||
name,
|
||||
x * 32 + 16,
|
||||
y * 32 + 16,
|
||||
alignWindow ?? false
|
||||
)
|
||||
.then(() => {
|
||||
callback?.();
|
||||
});
|
||||
};
|
||||
|
||||
maps.prototype.drawHeroAnimate = function (
|
||||
name: AnimationIds,
|
||||
callback?: () => void
|
||||
) {
|
||||
// @ts-ignore
|
||||
name = core.getMappedName(name);
|
||||
|
||||
// 正在播放录像或动画不存在:不显示动画
|
||||
if (core.isReplaying() || !core.material.animates[name]) {
|
||||
if (callback) callback();
|
||||
return -1;
|
||||
}
|
||||
|
||||
adapters.animate?.global('drawHeroAnimate', name).then(() => {
|
||||
callback?.();
|
||||
});
|
||||
|
||||
// 开始绘制
|
||||
// var animate = core.material.animates[name];
|
||||
// animate.se = animate.se || {};
|
||||
// if (typeof animate.se == 'string') animate.se = { 1: animate.se };
|
||||
|
||||
// var id = setTimeout(null);
|
||||
// core.status.animateObjs.push({
|
||||
// name: name,
|
||||
// id: id,
|
||||
// animate: animate,
|
||||
// hero: true,
|
||||
// index: 0,
|
||||
// callback: callback
|
||||
// });
|
||||
|
||||
// return id;
|
||||
};
|
||||
});
|
||||
|
||||
loading.once('loaded', () => {
|
||||
for (const animate of Object.values(core.material.animates)) {
|
||||
animate.se ??= {};
|
||||
if (typeof animate.se === 'string') {
|
||||
animate.se = { 1: animate.se };
|
||||
}
|
||||
animate.pitch ??= {};
|
||||
}
|
||||
});
|
||||
|
||||
return { readyMove, endMove, move };
|
||||
|
5
src/types/util.d.ts
vendored
5
src/types/util.d.ts
vendored
@ -753,7 +753,7 @@ type EventValuePreffix =
|
||||
|
||||
interface Animate {
|
||||
/**
|
||||
* 动画的帧数s
|
||||
* 动画的帧数
|
||||
*/
|
||||
frame: number;
|
||||
|
||||
@ -775,7 +775,8 @@ interface Animate {
|
||||
/**
|
||||
* 音效
|
||||
*/
|
||||
se: string;
|
||||
se: any;
|
||||
pitch: any;
|
||||
}
|
||||
|
||||
type Save = DeepReadonly<{
|
||||
|
Loading…
Reference in New Issue
Block a user