refactor: 客户端加载

This commit is contained in:
AncTe committed 2026-03-20 16:08:43 +08:00
1 parent fdeee99d40
commit 4fc6db3e79
55 files changed
+1699 -1234

No files matched your search

@@ -5,7 +5,7 @@ import { MotaOffscreenCanvas2D, SizedCanvasImageSource } from '@motajs/render';
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>;
type ImageMap = Record<ImageMapKeys, HTMLImageElement>;
type ImageMap = Record<ImageMapKeys, ImageBitmap>;
const i = (img: ImageMapKeys) => {
return core.material.images[img];
@@ -21,10 +21,10 @@ interface AutotileCache {
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
interface TextureRequire {
tileset: Record<string, HTMLImageElement>;
material: Record<ImageMapKeys, HTMLImageElement>;
tileset: Record<string, ImageBitmap>;
material: Record<ImageMapKeys, ImageBitmap>;
autotile: AutotileCaches;
images: Record<ImageIds, HTMLImageElement>;
images: Record<ImageIds, ImageBitmap>;
}
interface RenderableDataBase {
@@ -49,10 +49,10 @@ export interface AutotileRenderable extends RenderableDataBase {
}
class TextureCache {
tileset!: Record<string, HTMLImageElement>;
material: Record<ImageMapKeys, HTMLImageElement>;
tileset!: Record<string, ImageBitmap>;
material: Record<ImageMapKeys, ImageBitmap>;
autotile!: AutotileCaches;
images!: Record<ImageIds, HTMLImageElement>;
images!: Record<ImageIds, ImageBitmap>;
idNumberMap!: IdToNumber;
@@ -76,7 +76,7 @@ class TextureCache {
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
constructor() {
this.material = imageMap as Record<ImageMapKeys, HTMLImageElement>;
this.material = imageMap as Record<ImageMapKeys, ImageBitmap>;
}
init() {
@@ -11,6 +11,7 @@ import { GameTitleUI } from './ui/title';
import { createWeather } from './weather';
import { createMainExtension } from './commonIns';
import { createApp } from './renderer';
import { LoadSceneUI } from './ui/load';
export function createGameRenderer() {
const App = defineComponent(_props => {
@@ -23,6 +24,9 @@ export function createGameRenderer() {
mainRenderer.hide();
createApp(App).mount(mainRenderer);
sceneController.open(LoadSceneUI, {});
mainRenderer.show();
}
export function createRender() {
@@ -31,11 +35,6 @@ export function createRender() {
createAction();
createWeather();
loading.once('loaded', () => {
sceneController.open(GameTitleUI, {});
mainRenderer.show();
});
loading.once('assetBuilt', () => {
createMainExtension();
});
@@ -54,6 +54,8 @@ export const MOVING_TOLERANCE = 60;
/** 开关门动画的动画时长 */
export const DOOR_ANIMATE_INTERVAL = 50;
//#endregion
//#region 状态栏
/** 状态栏像素宽度 */
@@ -69,6 +71,8 @@ export const STATUS_BAR_COUNT = ENABLE_RIGHT_STATUS_BAR ? 2 : 1;
/** 状态栏宽度的一半 */
export const HALF_STATUS_WIDTH = STATUS_BAR_WIDTH / 2;
//#endregion
//#region 游戏画面
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
@@ -91,6 +95,8 @@ export const CENTER_LOC: ElementLocator = [
0.5
];
//#endregion
//#region 通用配置
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
@@ -98,6 +104,31 @@ export const POP_BOX_WIDTH = MAP_WIDTH / 2;
/** 默认字体 */
export const DEFAULT_FONT = new Font('Verdana', 16);
//#endregion
//#region 加载界面
/** 加载界面的任务进度条半径 */
export const LOAD_TASK_RADIUS = Math.min(MAIN_WIDTH, MAIN_HEIGHT) / 6;
/** 加载界面的字节进度条纵轴位置 */
export const LOAD_BYTE_HEIGHT = MAIN_HEIGHT / 2 + MAIN_HEIGHT / 4;
/** 加载界面任务进度条的纵轴位置 */
export const LOAD_TASK_CENTER_HEIGHT = MAIN_HEIGHT / 2 - MAIN_HEIGHT / 8;
/** 加载界面字节进度条的长度 */
export const LOAD_BYTE_LENGTH = MAIN_WIDTH - MAIN_WIDTH / 12;
/** 加载界面任务进度条的粗细 */
export const LOAD_TASK_LINE_WIDTH = 6;
/** 加载界面字节进度条的粗细 */
export const LOAD_BYTE_LINE_WIDTH = 6;
/** 已加载部分进度条的颜色 */
export const LOAD_LOADED_COLOR = '#57ff78';
/** 未加载部分进度条的颜色 */
export const LOAD_UNLOADED_COLOR = '#ccc';
/** 加载界面的文字颜色 */
export const LOAD_FONT_COLOR = '#fff';
//#endregion
//#region 存档界面
/** 存档缩略图尺寸 */
@@ -115,8 +146,13 @@ export const SAVE_DOWN_PAD = 30;
/** 存档页码数,调高并不会影响性能,但是如果玩家存档太多的话会导致存档体积很大 */
export const SAVE_PAGES = 1000;
//#endregion
//#region 标题界面
/** 标题图 */
export const TITLE_BACKGROUND_IMAGE = 'bg.jpg';
/** 标题文字中心横坐标 */
export const TITLE_X = HALF_WIDTH;
/** 标题文字中心纵坐标 */
@@ -136,3 +172,5 @@ export const BUTTONS_HEIGHT = 200;
export const BUTTONS_X = HALF_WIDTH;
/** 标题界面按钮左上角纵坐标 */
export const BUTTONS_Y = MAIN_HEIGHT - BUTTONS_HEIGHT;
//#endregion
@@ -0,0 +1,165 @@
import { DefaultProps } from '@motajs/render-vue';
import {
GameUI,
SetupComponentOptions,
UIComponentProps
} from '@motajs/system';
import { defineComponent } from 'vue';
import {
FULL_LOC,
LOAD_BYTE_HEIGHT,
LOAD_BYTE_LENGTH,
LOAD_BYTE_LINE_WIDTH,
LOAD_FONT_COLOR,
LOAD_LOADED_COLOR,
LOAD_TASK_CENTER_HEIGHT,
LOAD_TASK_LINE_WIDTH,
LOAD_TASK_RADIUS,
LOAD_UNLOADED_COLOR,
MAIN_WIDTH
} from '../shared';
import { ElementLocator, Font, MotaOffscreenCanvas2D } from '@motajs/render';
import { transitioned } from '../use';
import { cosh, CurveMode, linear } from '@motajs/animate';
import { loader } from '@user/client-base';
import { clamp } from 'lodash-es';
import { sleep } from '@motajs/common';
import { loading } from '@user/data-base';
import { GameTitleUI } from './title';
export interface ILoadProps extends UIComponentProps, DefaultProps {}
const loadSceneProps = {
props: ['controller', 'instance']
} satisfies SetupComponentOptions<ILoadProps>;
export const LoadScene = defineComponent<ILoadProps>(props => {
const taskFont = new Font('Verdana', 24);
const byteFont = new Font('Verdana', 12);
/** 当前加载进度 */
const taskProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!;
const byteProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!;
const alpha = transitioned(1, 400, linear())!;
// 两个进度条的位置
const taskLoc: ElementLocator = [
MAIN_WIDTH / 2,
LOAD_TASK_CENTER_HEIGHT,
LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2,
LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2,
0.5,
0.5
];
const byteLoc: ElementLocator = [
MAIN_WIDTH / 2,
LOAD_BYTE_HEIGHT,
LOAD_BYTE_LENGTH + LOAD_BYTE_LINE_WIDTH,
LOAD_BYTE_LINE_WIDTH * 2 + byteFont.size,
0.5,
0.5
];
const loadEnd = async () => {
loading.emit('loaded');
alpha.set(0);
await sleep(400);
props.controller.closeAll();
props.controller.open(GameTitleUI, {});
};
const startLoad = async () => {
loader.initSystemLoadTask();
loader.load().then(() => {
loadEnd();
});
for await (const _ of loader.progress) {
taskProgress.set(loader.progress.getLoadedTasks());
byteProgress.set(loader.progress.getLoadedByte());
}
};
// 开始加载
startLoad();
/** 渲染加载任务进度 */
const renderTaskList = (canvas: MotaOffscreenCanvas2D) => {
const ctx = canvas.ctx;
ctx.lineCap = 'round';
ctx.lineWidth = LOAD_TASK_LINE_WIDTH;
ctx.font = taskFont.string();
const loaded = loader.progress.getLoadedTasks();
const total = loader.progress.getAddedTasks();
// 这里使用渐变参数,因为要有动画效果
const progress = clamp(taskProgress.value / total, 0, 1);
const cx = taskLoc[2]! / 2;
const cy = taskLoc[3]! / 2;
ctx.beginPath();
ctx.arc(cx, cy, LOAD_TASK_RADIUS, 0, Math.PI * 2);
ctx.strokeStyle = LOAD_UNLOADED_COLOR;
ctx.stroke();
ctx.beginPath();
const end = progress * Math.PI * 2 - Math.PI / 2;
ctx.arc(cx, cy, LOAD_TASK_RADIUS, -Math.PI / 2, end);
ctx.strokeStyle = LOAD_LOADED_COLOR;
ctx.stroke();
ctx.fillStyle = LOAD_FONT_COLOR;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${loaded} / ${total}`, cx, cy + 3);
};
/** 渲染加载字节进度 */
const renderByteList = (canvas: MotaOffscreenCanvas2D) => {
const ctx = canvas.ctx;
ctx.lineCap = 'round';
ctx.lineWidth = LOAD_BYTE_LINE_WIDTH;
ctx.font = byteFont.string();
const total = loader.progress.getTotalByte();
const loaded = loader.progress.getLoadedByte();
// 这里使用渐变参数,因为要有动画效果
const progress = clamp(byteProgress.value / total, 0, 1);
const sx = LOAD_BYTE_LINE_WIDTH;
const sy = byteFont.size + LOAD_BYTE_LINE_WIDTH;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + LOAD_BYTE_LENGTH, sy);
ctx.strokeStyle = LOAD_UNLOADED_COLOR;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + progress * LOAD_BYTE_LENGTH, sy);
ctx.strokeStyle = LOAD_LOADED_COLOR;
ctx.stroke();
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillStyle = LOAD_FONT_COLOR;
const loadedMB = (loaded / 2 ** 20).toFixed(2);
const totalMB = (total / 2 ** 20).toFixed(2);
const percent = loader.progress.getByteRatio() * 100;
ctx.fillText(
`${loadedMB}MB / ${totalMB}MB | ${percent.toFixed(2)}%`,
byteLoc[2]! - LOAD_BYTE_LINE_WIDTH,
byteLoc[3]! - LOAD_BYTE_LINE_WIDTH * 2
);
};
return () => (
<container loc={FULL_LOC} alpha={alpha.ref.value}>
<custom
loc={taskLoc}
render={renderTaskList}
bindings={[taskProgress.ref]}
nocache
/>
<custom
loc={byteLoc}
render={renderByteList}
bindings={[byteProgress.ref]}
nocache
/>
</container>
);
}, loadSceneProps);
export const LoadSceneUI = new GameUI('load-scene', LoadScene);
@@ -1,7 +1,7 @@
import { GameUI, SetupComponentOptions } from '@motajs/system';
import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue';
import { TextContent } from '../components';
import { ElementLocator, Font, SizedCanvasImageSource } from '@motajs/render';
import { ElementLocator, Font, ITexture } from '@motajs/render';
import { MixedToolbar, ReplayingStatus } from './toolbar';
import { openViewMap } from './viewmap';
import { mainUIController } from './controller';
@@ -12,6 +12,7 @@ import {
STATUS_BAR_WIDTH
} from '../shared';
import { DefaultProps } from '@motajs/render-vue';
import { materials } from '@user/client-base';
export interface ILeftHeroStatus {
/** 楼层 id */
@@ -69,27 +70,27 @@ export interface IRightHeroStatus {
interface StatusInfo {
/** 图标 */
icon: SizedCanvasImageSource;
readonly icon: ITexture | null;
/** 属性值,经过格式化 */
value: ComputedRef<string>;
readonly value: ComputedRef<string>;
/** 字体 */
font: Font;
readonly font: Font;
/** 文字颜色 */
color: CanvasStyle;
readonly color: CanvasStyle;
}
interface KeyLikeItem {
/** 属性值,经过格式化 */
value: ComputedRef<string>;
readonly value: ComputedRef<string>;
/** 字体 */
font: Font;
readonly font: Font;
/** 文字颜色 */
color: CanvasStyle;
readonly color: CanvasStyle;
}
interface KeyLikeInfo {
/** 这一行包含的内容 */
items: KeyLikeItem[];
readonly items: KeyLikeItem[];
}
interface StatusBarProps<T> extends DefaultProps {
@@ -115,15 +116,15 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
/** 状态属性的开始纵坐标 */
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
// 可以换成 core.material.images.images['xxx.png'] 来使用全塔属性注册的图片
const hpIcon = core.statusBar.icons.hp;
const atkIcon = core.statusBar.icons.atk;
const defIcon = core.statusBar.icons.def;
const mdefIcon = core.statusBar.icons.mdef;
const moneyIcon = core.statusBar.icons.money;
const expIcon = core.statusBar.icons.exp;
const manaIcon = core.statusBar.icons.mana;
const lvIcon = core.statusBar.icons.lv;
// 可以换成 materials.getImageByAlias('xxx.png') 来使用全塔属性注册的图片
const hpIcon = materials.getImageByAlias('icon-hp');
const atkIcon = materials.getImageByAlias('icon-atk');
const defIcon = materials.getImageByAlias('icon-def');
const mdefIcon = materials.getImageByAlias('icon-mdef');
const moneyIcon = materials.getImageByAlias('icon-money');
const expIcon = materials.getImageByAlias('icon-exp');
const manaIcon = materials.getImageByAlias('icon-mana');
const lvIcon = materials.getImageByAlias('icon-lv');
const s = p.status;
@@ -14,6 +14,7 @@ import {
HALF_WIDTH,
MAIN_HEIGHT,
MAIN_WIDTH,
TITLE_BACKGROUND_IMAGE,
TITLE_FILL,
TITLE_STROKE,
TITLE_STROKE_WIDTH,
@@ -34,6 +35,7 @@ import { MainSceneUI } from './main';
import { adjustCover } from '../utils';
import { cosh, CurveMode, linear } from '@motajs/animate';
import { sleep } from '@motajs/common';
import { materials } from '@user/client-base';
const enum TitleButton {
StartGame,
@@ -62,12 +64,12 @@ const gameTitleProps = {
} satisfies SetupComponentOptions<GameTitleProps>;
export const GameTitle = defineComponent<GameTitleProps>(props => {
const bg = core.material.images.images['bg.jpg'];
const bg = materials.getImageByAlias(TITLE_BACKGROUND_IMAGE);
//#region 计算背景图
const [width, height] = adjustCover(
bg.width,
bg.height,
bg?.width ?? MAIN_WIDTH,
bg?.height ?? MAIN_HEIGHT,
MAIN_WIDTH,
MAIN_HEIGHT
);
@@ -25,6 +25,7 @@ import { MAIN_HEIGHT, FULL_LOC, POP_BOX_WIDTH, CENTER_LOC } from '../shared';
import { openReplay, openSettings } from './settings';
import { openViewMap } from './viewmap';
import { DefaultProps } from '@motajs/render-vue';
import { materials } from '@user/client-base';
interface ToolbarProps extends DefaultProps {
loc?: ElementLocator;
@@ -73,15 +74,15 @@ export const PlayingToolbar = defineComponent<
ToolbarEmits,
keyof ToolbarEmits
>((props, { emit }) => {
const bookIcon = core.statusBar.icons.book;
const flyIcon = core.statusBar.icons.fly;
const toolIcon = core.statusBar.icons.toolbox;
const equipIcon = core.statusBar.icons.equipbox;
const keyIcon = core.statusBar.icons.keyboard;
const shopIcon = core.statusBar.icons.shop;
const saveIcon = core.statusBar.icons.save;
const loadIcon = core.statusBar.icons.load;
const setIcon = core.statusBar.icons.settings;
const bookIcon = materials.getImageByAlias('icon-book');
const flyIcon = materials.getImageByAlias('icon-fly');
const toolIcon = materials.getImageByAlias('icon-toolbox');
const equipIcon = materials.getImageByAlias('icon-equipbox');
const keyIcon = materials.getImageByAlias('icon-keyboard');
const shopIcon = materials.getImageByAlias('icon-shop');
const saveIcon = materials.getImageByAlias('icon-save');
const loadIcon = materials.getImageByAlias('icon-load');
const setIcon = materials.getImageByAlias('icon-settings');
const iconFont = new Font('Verdana', 12);
@@ -170,8 +171,8 @@ const replayingProps = {
export const ReplayingToolbar = defineComponent<ReplayingProps>(props => {
const status = props.status;
const bookIcon = core.statusBar.icons.book;
const saveIcon = core.statusBar.icons.save;
const bookIcon = materials.getImageByAlias('icon-book');
const saveIcon = materials.getImageByAlias('icon-save');
const font1 = Font.defaults({ size: 16 });
const font2 = new Font('Verdana', 12);
@@ -4,7 +4,7 @@ import { clamp } from 'lodash-es';
export class SunWeather extends Weather<CustomRenderItem> {
/** 阳光图片 */
private image: HTMLImageElement | null = null;
private image: ImageBitmap | null = null;
/** 阳光图片的不透明度 */
private alpha: number = 0;
/** 阳光的最大不透明度 */