feat: 视角控制系统 & fix: moveViewport

This commit is contained in:
unanmed 2024-10-04 00:58:20 +08:00
parent 7cc4be606a
commit 5970c85dff
12 changed files with 459 additions and 37 deletions

View File

@ -1533,7 +1533,7 @@ control.prototype.chooseReplayFile = function () {
function (obj) {
if (obj.name != core.firstData.name)
return alert('存档和游戏不一致!');
if (!obj.route) return alert('无效的录像!');
if (!obj.route) return core.drawTip('无效的录像!');
var _replay = function () {
core.startGame(
core.flags.startUsingCanvas ? '' : obj.hard || '',
@ -2370,7 +2370,7 @@ control.prototype._doSL_load = function (id, callback) {
},
function (err) {
console.error(err);
alert('无效的存档');
core.drawTip('无效的存档');
}
);
}
@ -2393,7 +2393,7 @@ control.prototype._doSL_reload = function (id, callback) {
};
control.prototype._doSL_load_afterGet = function (id, data) {
if (!data) return alert('无效的存档');
if (!data) return core.drawTip('无效的存档');
var _replay = function () {
core.startGame(
data.hard,

View File

@ -161,15 +161,12 @@ main.floors.MT16=
},
{
"type": "animate",
"name": "amazed",
"async": true
"name": "amazed"
},
{
"type": "sleep",
"time": 1000
},
{
"type": "waitAsync"
"time": 1000,
"noSkip": true
},
{
"type": "moveHero",

View File

@ -69,10 +69,12 @@ import { Image, Text } from './render/preset/misc';
import { RenderItem } from './render/item';
import { texture } from './render/cache';
import { RenderAdapter } from './render/adapter';
import { getMainRenderer } from './render';
import { Layer } from './render/preset/layer';
import { LayerGroupFloorBinder } from './render/preset/floor';
import { HeroKeyMover } from './main/action/move';
import { Camera } from './render/camera';
import * as Animation from 'mutate-animate';
import './render/index';
// ----- 类注册
Mota.register('class', 'AudioPlayer', AudioPlayer);
@ -152,7 +154,6 @@ Mota.register('module', 'Effect', {
});
Mota.register('module', 'Render', {
texture,
getMainRenderer: getMainRenderer,
MotaRenderer,
Container,
Sprite,
@ -161,11 +162,13 @@ Mota.register('module', 'Render', {
RenderItem,
RenderAdapter,
Layer,
LayerGroupFloorBinder
LayerGroupFloorBinder,
Camera
});
Mota.register('module', 'Action', {
HeroKeyMover
});
Mota.register('module', 'Animation', Animation);
main.renderLoaded = true;
Mota.require('var', 'hook').emit('renderLoaded');

335
src/core/render/camera.ts Normal file
View File

@ -0,0 +1,335 @@
import { Animation, Transition } from 'mutate-animate';
import { RenderItem } from './item';
import { logger } from '../common/logger';
import { Transform } from './transform';
interface CameraTranslate {
readonly type: 'translate';
readonly from: Camera;
x: number;
y: number;
}
interface CameraRotate {
readonly type: 'rotate';
readonly from: Camera;
/** 旋转角,单位弧度 */
angle: number;
}
interface CameraScale {
readonly type: 'scale';
readonly from: Camera;
x: number;
y: number;
}
type CameraOperation = CameraTranslate | CameraScale | CameraRotate;
export class Camera {
/** 当前绑定的渲染元素 */
readonly binded: RenderItem;
/** 目标变换矩阵,默认与 `this.binded.transform` 同引用 */
transform: Transform;
/** 委托ticker的id */
private delegation: number;
/** 所有的动画id */
private animationIds: Set<number> = new Set();
/** 是否需要更新视角 */
private needUpdate: boolean = false;
/** 变换操作列表,因为矩阵乘法跟顺序有关,因此需要把各个操作拆分成列表进行 */
protected operation: CameraOperation[] = [];
/** 渲染元素到摄像机的映射 */
private static cameraMap: Map<RenderItem, Camera> = new Map();
/**
* 使`new Camera`
* @param item
*/
static for(item: RenderItem) {
const camera = this.cameraMap.get(item);
if (!camera) {
const ca = new Camera(item);
this.cameraMap.set(item, ca);
return ca;
} else {
return camera;
}
}
constructor(item: RenderItem) {
this.binded = item;
this.delegation = item.delegateTicker(() => this.tick());
this.transform = item.transform;
item.on('destroy', () => {
this.destroy();
});
if (Camera.cameraMap.has(item)) {
logger.warn(22);
}
}
private tick = () => {
if (!this.needUpdate) return;
const trans = this.transform;
trans.reset();
for (const o of this.operation) {
if (o.type === 'translate') {
trans.translate(o.x, o.y);
} else if (o.type === 'rotate') {
trans.rotate(o.angle);
} else {
trans.scale(o.x, o.y);
}
}
this.binded.update(this.binded);
this.needUpdate = false;
};
/**
*
*/
requestUpdate() {
this.needUpdate = true;
}
/**
*
* @param operation
*/
removeOperation(operation: CameraOperation) {
const index = this.operation.indexOf(operation);
if (index === -1) return;
this.operation.splice(index, 1);
}
/**
*
*/
clearOperation() {
this.operation.splice(0);
}
/**
*
* @returns
*/
addTranslate(): CameraTranslate {
const item: CameraTranslate = {
type: 'translate',
x: 0,
y: 0,
from: this
};
this.operation.push(item);
return item;
}
/**
*
* @returns
*/
addRotate(): CameraRotate {
const item: CameraRotate = {
type: 'rotate',
angle: 0,
from: this
};
this.operation.push(item);
return item;
}
/**
*
* @returns
*/
addScale(): CameraScale {
const item: CameraScale = {
type: 'scale',
x: 0,
y: 0,
from: this
};
this.operation.push(item);
return item;
}
/**
*
* @param time
* @param update
*/
applyAnimation(time: number, update: () => void) {
const delegation = this.binded.delegateTicker(
() => {
update();
this.needUpdate = true;
},
time,
() => {
update();
this.needUpdate = true;
this.animationIds.delete(delegation);
}
);
this.animationIds.add(delegation);
}
/**
*
* @param operation
* @param animate
* @param time
*/
applyTranslateAnimation(
operation: CameraTranslate,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.x = animate.x;
operation.y = animate.y;
};
this.applyAnimation(time, update);
}
/**
*
* @param operation
* @param animate
* @param time
*/
applyRotateAnimation(
operation: CameraRotate,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.angle = animate.angle;
};
this.applyAnimation(time, update);
}
/**
*
* @param operation
* @param animate
* @param time
*/
applyScaleAnimation(
operation: CameraScale,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.x = animate.size;
operation.y = animate.size;
};
this.applyAnimation(time, update);
}
/**
* 使 x,y `transition.value.x``transition.value.y`
* @param operation
* @param animate
* @param time
*/
applyTranslateTransition(
operation: CameraTranslate,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.x = animate.value.x;
operation.y = animate.value.y;
};
this.applyAnimation(time, update);
}
/**
* 使 angle `transition.value.angle`
* @param operation
* @param animate
* @param time
*/
applyRotateTransition(
operation: CameraRotate,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.angle = animate.value.angle;
};
this.applyAnimation(time, update);
}
/**
* 使 size `transition.value.size`
* @param operation
* @param animate
* @param time
*/
applyScaleTransition(
operation: CameraScale,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.x = animate.value.size;
operation.y = animate.value.size;
};
this.applyAnimation(time, update);
}
/**
* 使
*/
destroy() {
this.binded.removeTicker(this.delegation);
this.animationIds.forEach(v => this.binded.removeTicker(v));
Camera.cameraMap.delete(this.binded);
}
}

View File

@ -1,6 +1,6 @@
import { FloorItemDetail } from '@/plugin/fx/itemDetail';
import { FloorDamageExtends } from './preset/damage';
import { LayerDoorAnimate, LayerGroupFloorBinder } from './preset/floor';
import { LayerDoorAnimate } from './preset/floor';
import { HeroRenderer } from './preset/hero';
import { LayerGroup, FloorLayer } from './preset/layer';
import { MotaRenderer } from './render';
@ -14,10 +14,6 @@ import { Container } from './container';
let main: MotaRenderer;
export function getMainRenderer() {
return main;
}
Mota.require('var', 'loading').once('loaded', () => {
const render = new MotaRenderer();
main = render;
@ -29,6 +25,10 @@ Mota.require('var', 'loading').once('loaded', () => {
mapDraw.id = 'map-draw';
layer.id = 'layer-main';
mapDraw.setHD(true);
mapDraw.setAntiAliasing(false);
mapDraw.size(core._PX_, core._PY_);
['bg', 'bg2', 'event', 'fg', 'fg2'].forEach(v => {
layer.addLayer(v as FloorLayer);
});

View File

@ -128,6 +128,7 @@ export class FloorViewport implements ILayerGroupRenderExtends {
* @param y
*/
setPosition(x: number, y: number) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.group.removeTicker(this.transition, false);
this.nx = nx;
@ -140,6 +141,7 @@ export class FloorViewport implements ILayerGroupRenderExtends {
* @param y
*/
moveTo(x: number, y: number) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
if (this.inTransition) {
const distance = Math.hypot(this.nx - nx, this.ny - ny);
@ -176,6 +178,7 @@ export class FloorViewport implements ILayerGroupRenderExtends {
* @param y
*/
mutateTo(x: number, y: number) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.createTransition(nx, ny, this.transitionTime);
}
@ -321,6 +324,7 @@ export class FloorViewport implements ILayerGroupRenderExtends {
const halfWidth = core._PX_ / 2;
const halfHeight = core._PY_ / 2;
this.delegation = this.group.delegateTicker(() => {
if (!this.enabled) return;
if (this.nx === nx && this.ny === ny) return;
const cell = this.group.cellSize;
const half = cell / 2;

View File

@ -4,7 +4,7 @@ import { RenderItem } from './item';
import { Transform } from './transform';
export class MotaRenderer extends Container {
static list: Set<MotaRenderer> = new Set();
static list: Map<string, MotaRenderer> = new Map();
target: MotaCanvas2D;
@ -13,18 +13,17 @@ export class MotaRenderer extends Container {
constructor(id: string = 'render-main') {
super('static', false);
this.id = id;
this.target = new MotaCanvas2D(id);
this.size(core._PX_, core._PY_);
this.target.withGameScale(true);
this.target.size(core._PX_, core._PY_);
this.target.css(`z-index: 100`);
this.target.setAntiAliasing(false);
this.setAnchor(0.5, 0.5);
this.transform.translate(240, 240);
MotaRenderer.list.add(this);
MotaRenderer.list.set(id, this);
}
update(item?: RenderItem) {
@ -80,7 +79,11 @@ export class MotaRenderer extends Container {
}
destroy() {
MotaRenderer.list.delete(this);
MotaRenderer.list.delete(this.id);
}
static get(id: string) {
return this.list.get(id);
}
}

View File

@ -38,7 +38,7 @@ export class Transform {
/**
*
*/
move(x: number, y: number) {
translate(x: number, y: number) {
mat3.translate(this.mat, this.mat, [x, y]);
this.x += x;
this.y += y;

View File

@ -40,6 +40,10 @@
"17": "Floor-damage extension needs 'floor-binder' extension as dependency.",
"18": "Uncaught error in posting like info for danmaku. Danmaku id: $1.",
"19": "Repeat light id: '$1'.",
"20": "Cannot apply animation to camera operation that is not belong to it.",
"21": "Cannot apply transition to camera operation that is not belong to it.",
"22": "There is already a camera for delivered render item. Consider using 'Camera.for' to avoid some exceptions.",
"23": "Render item with id of '$1' has already exists.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
}
}

View File

@ -443,6 +443,9 @@ export class HeroMover extends ObjectMoverBase {
/** 是否会在特殊时刻进行自动存档 */
private autoSave: boolean = false;
/** 本次移动开始时的移动速度 */
private beforeMoveSpeed: number = 100;
/** 这一步的传送门信息 */
private portalData?: BluePalace.PortalTo;
@ -452,6 +455,7 @@ export class HeroMover extends ObjectMoverBase {
inLockControl: boolean = false,
autoSave: boolean = false
): IMoveController | null {
if (this.moving) return null;
this.ignoreTerrain = ignoreTerrain;
this.noRoute = noRoute;
this.inLockControl = inLockControl;
@ -472,6 +476,7 @@ export class HeroMover extends ObjectMoverBase {
}
protected async onMoveStart(controller: IMoveController): Promise<void> {
this.beforeMoveSpeed = this.moveSpeed;
const adapter = HeroMover.adapter;
if (!adapter) return;
await adapter.all('readyMove');
@ -479,6 +484,8 @@ export class HeroMover extends ObjectMoverBase {
}
protected async onMoveEnd(controller: IMoveController): Promise<void> {
this.moveSpeed = this.beforeMoveSpeed;
this.onSetMoveSpeed(this.moveSpeed, controller);
const adapter = HeroMover.adapter;
if (!adapter) return;
await adapter.all('endMove');

View File

@ -38,6 +38,8 @@ import type { Layer } from '@/core/render/preset/layer';
import type { LayerGroupFloorBinder } from '@/core/render/preset/floor';
import type { HeroKeyMover } from '@/core/main/action/move';
import type { BlockMover, HeroMover, ObjectMoverBase } from './state/move';
import type { Camera } from '@/core/render/camera';
import type * as Animation from 'mutate-animate';
interface ClassInterface {
// 渲染进程与游戏进程通用
@ -119,6 +121,7 @@ interface ModuleInterface {
RenderAdapter: typeof RenderAdapter;
Layer: typeof Layer;
LayerGroupFloorBinder: typeof LayerGroupFloorBinder;
Camera: typeof Camera;
};
State: {
ItemState: typeof ItemState;
@ -133,6 +136,7 @@ interface ModuleInterface {
Action: {
HeroKeyMover: typeof HeroKeyMover;
};
Animation: typeof Animation;
}
interface SystemInterfaceMap {
@ -148,20 +152,10 @@ interface PluginInterface {
// 渲染进程定义的插件
pop_r: typeof import('../plugin/pop');
use_r: typeof import('../plugin/use');
// animate: typeof import('../plugin/animateController');
// utils: typeof import('../plugin/utils');
// status: typeof import('../plugin/ui/statusBar');
fly_r: typeof import('../plugin/ui/fly');
chase_r: typeof import('../plugin/chase/chase');
// webglUtils: typeof import('../plugin/webgl/utils');
// shadow_r: typeof import('../plugin/shadow/shadow');
// gameShadow_r: typeof import('../plugin/shadow/gameShadow');
// achievement: typeof import('../plugin/ui/achievement');
completion_r: typeof import('../plugin/completion');
// path: typeof import('../plugin/fx/path');
gameCanvas_r: typeof import('../plugin/fx/gameCanvas');
// noise: typeof import('../plugin/fx/noise');
smooth_r: typeof import('../plugin/fx/smoothView');
frag_r: typeof import('../plugin/fx/frag');
// 游戏进程定义的插件
utils_g: typeof import('../plugin/game/utils');
@ -174,12 +168,9 @@ interface PluginInterface {
chase_g: typeof import('../plugin/game/chase');
skill_g: typeof import('../plugin/game/skill');
towerBoss_g: typeof import('../plugin/game/towerBoss');
// heroFourFrames_g: typeof import('../plugin/game/fx/heroFourFrames');
rewrite_g: typeof import('../plugin/game/fx/rewrite');
itemDetail_g: typeof import('../plugin/game/fx/itemDetail');
checkBlock_g: typeof import('../plugin/game/enemy/checkblock');
// halo_g: typeof import('../plugin/game/fx/halo');
// study_g: typeof import('../plugin/game/study');
}
interface PackageInterface {

View File

@ -5,7 +5,7 @@ import type {
LayerFloorBinder
} from '@/core/render/preset/floor';
import type { HeroRenderer } from '@/core/render/preset/hero';
import type { Layer } from '@/core/render/preset/layer';
import type { Layer, LayerGroup } from '@/core/render/preset/layer';
import type { TimingFn } from 'mutate-animate';
import { BlockMover, heroMoveCollection, MoveStep } from '@/game/state/move';
import type { FloorViewport } from '@/core/render/preset/viewport';
@ -85,6 +85,11 @@ export function init() {
}
Mota.r(() => {
// ----- 引入
const Camera = Mota.require('module', 'Render').Camera;
const Renderer = Mota.require('module', 'Render').MotaRenderer;
const Animation = Mota.require('module', 'Animation');
// ----- 勇士移动相关
control.prototype.moveAction = async function (callback?: () => void) {
heroMover.clearMoveQueue();
@ -578,6 +583,79 @@ export function init() {
if (success) adapters.viewport?.all('mutateTo', destX, destY);
return success;
};
control.prototype.moveViewport = function (
x: number,
y: number,
_moveMode: EaseMode,
time: number = 0,
callback?: () => void
) {
const main = Renderer.get('render-main');
const layer = main?.getElementById('layer-main') as LayerGroup;
if (!layer) return;
const camera = Camera.for(layer);
camera.clearOperation();
const translate = camera.addTranslate();
const animateTime = time / Math.max(core.status.replay.speed, 1);
const animate = new Animation.Animation();
animate
.absolute()
.time(1)
.mode(Animation.linear())
.move(-core.bigmap.offsetX, -core.bigmap.offsetY);
animate.time(animateTime).move(-x * 32, -y * 32);
camera.applyTranslateAnimation(
translate,
animate,
animateTime + 50
);
camera.transform = layer.camera;
const timeout = window.setTimeout(() => {
core.bigmap.offsetX = x * 32;
core.bigmap.offsetY = y * 32;
callback?.();
}, animateTime + 50);
// time /= Math.max(core.status.replay.speed, 1);
// var per_time = 10,
// step = 0,
// steps = parseInt(time / per_time);
// if (steps <= 0) {
// this.setViewport(32 * x, 32 * y);
// if (callback) callback();
// return;
// }
// var px = core.clamp(32 * x, 0, 32 * core.bigmap.width - core._PX_);
// var py = core.clamp(32 * y, 0, 32 * core.bigmap.height - core._PY_);
// var cx = core.bigmap.offsetX;
// var cy = core.bigmap.offsetY;
// var moveFunc = core.applyEasing(moveMode);
// var animate = window.setInterval(function () {
// step++;
// core.setViewport(
// cx + moveFunc(step / steps) * (px - cx),
// cy + moveFunc(step / steps) * (py - cy)
// );
// if (step == steps) {
// delete core.animateFrame.asyncId[animate];
// clearInterval(animate);
// core.setViewport(px, py);
// if (callback) callback();
// }
// }, per_time);
const id = fallbackIds++;
core.animateFrame.lastAsyncId = id;
core.animateFrame.asyncId[id] = () => {
callback?.();
clearTimeout(timeout);
};
};
});
loading.once('loaded', () => {