前言

微信小游戏凭借其无需下载、即点即玩的特点,已成为移动游戏的重要形态。但小游戏的包体积限制(4MB首包)、内存限制等约束,对架构设计提出了更高要求。本文将深入探讨小游戏架构设计的最佳实践。

游戏引擎选择与架构

1. 引擎选型对比

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 引擎适配层设计
interface GameEngine {
    // 初始化
    init(): Promise<void>;

    // 场景管理
    loadScene(sceneId: string): Promise<void>;

    // 资源管理
    loadResource(url: string): Promise<any>;
    releaseResource(url: string): void;

    // 渲染
    render(): void;

    // 事件
    on(event: string, handler: Function): void;
    off(event: string, handler: Function): void;
}

// Cocos Creator 适配器
class CocosAdapter implements GameEngine {
    private director: cc.Director;

    async init(): Promise<void> {
        // 初始化 Cocos 引擎
        this.director = cc.director;
        this.director.setDisplayStats(true);

        // 配置小游戏特有设置
        this.configureForMiniGame();
    }

    private configureForMiniGame(): void {
        // 启用合图
        cc.macro.CLEANUP_IMAGE_CACHE = false;

        // 优化渲染
        cc.dynamicAtlasManager.enabled = true;

        // 内存管理
        cc.macro.ENABLE_TRANSPARENT_CANVAS = false;
    }

    async loadResource(url: string): Promise<any> {
        return new Promise((resolve, reject) => {
            cc.resources.load(url, (err, asset) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(asset);
                }
            });
        });
    }
}

// LayaAir 适配器
class LayaAdapter implements GameEngine {
    private stage: Laya.Stage;

    async init(): Promise<void> {
        Laya.init(
            Laya.Browser.width,
            Laya.Browser.height,
            WebGL
        );

        this.stage = Laya.stage;
        this.stage.scaleMode = Stage.SCALE_FIXED_AUTO;
        this.stage.screenMode = Stage.SCREEN_VERTICAL;

        // 小游戏优化
        Laya.ResourceVersion.enable("version.json", Laya.ResourceVersion.FILENAME_VERSION);
    }

    async loadResource(url: string): Promise<any> {
        return await Laya.loader.load(url);
    }
}

// 引擎工厂
class EngineFactory {
    static create(type: 'cocos' | 'laya' | 'egret'): GameEngine {
        switch (type) {
            case 'cocos':
                return new CocosAdapter();
            case 'laya':
                return new LayaAdapter();
            case 'egret':
                return new EgretAdapter();
            default:
                throw new Error(`Unsupported engine: ${type}`);
        }
    }
}

2. 模块化架构

  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
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// 模块系统
interface GameModule {
    readonly name: string;
    readonly dependencies: string[];

    onLoad(): void | Promise<void>;
    onRelease(): void | Promise<void>;
    onUpdate(dt: number): void;
}

// 模块管理器
class ModuleManager {
    private modules = new Map<string, GameModule>();
    private loadedModules = new Set<string>();
    private updateOrder: string[] = [];

    register(module: GameModule): void {
        this.modules.set(module.name, module);

        // 更新更新顺序(拓扑排序)
        this.updateOrder = this.topologicalSort();
    }

    async load(moduleName: string): Promise<void> {
        const module = this.modules.get(moduleName);
        if (!module) {
            throw new Error(`Module not found: ${moduleName}`);
        }

        // 加载依赖
        for (const dep of module.dependencies) {
            if (!this.loadedModules.has(dep)) {
                await this.load(dep);
            }
        }

        // 加载模块
        await module.onLoad();
        this.loadedModules.add(moduleName);
    }

    update(dt: number): void {
        for (const name of this.updateOrder) {
            if (this.loadedModules.has(name)) {
                const module = this.modules.get(name)!;
                module.onUpdate(dt);
            }
        }
    }

    private topologicalSort(): string[] {
        const sorted: string[] = [];
        const visited = new Set<string>();
        const visiting = new Set<string>();

        const visit = (name: string) => {
            if (visited.has(name)) return;
            if (visiting.has(name)) {
                throw new Error(`Circular dependency detected: ${name}`);
            }

            visiting.add(name);

            const module = this.modules.get(name);
            if (module) {
                for (const dep of module.dependencies) {
                    visit(dep);
                }
            }

            visiting.delete(name);
            visited.add(name);
            sorted.push(name);
        };

        for (const name of this.modules.keys()) {
            visit(name);
        }

        return sorted;
    }
}

// 核心模块示例
class UIModule implements GameModule {
    readonly name = 'ui';
    readonly dependencies = ['asset'];

    private uiStack: UIWidget[] = [];

    async onLoad(): Promise<void> {
        // 初始化 UI 系统
        cc.macro.CLEANUP_IMAGE_CACHE = false;
    }

    pushWidget(widget: UIWidget): void {
        this.uiStack.push(widget);
        widget.show();
    }

    popWidget(): void {
        const widget = this.uiStack.pop();
        if (widget) {
            widget.hide();
        }
    }

    onUpdate(dt: number): void {
        // UI 更新逻辑
    }

    onRelease(): void {
        this.uiStack.forEach(w => w.destroy());
        this.uiStack = [];
    }
}

资源管理与分包加载

1. 智能分包策略

  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
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// 分包配置
interface SubPackageConfig {
    name: string;
    root: string;
    pages: string[];
    independent?: boolean;
    lazy?: boolean;
}

// 分包管理器
class SubPackageManager {
    private packages = new Map<string, SubPackageInfo>();
    private loadedPackages = new Set<string>();
    private loadingPromises = new Map<string, Promise<void>>();

    configure(configs: SubPackageConfig[]): void {
        for (const config of configs) {
            this.packages.set(config.name, {
                ...config,
                size: 0,
                priority: this.calculatePriority(config)
            });
        }
    }

    async loadPackage(packageName: string): Promise<void> {
        // 已加载
        if (this.loadedPackages.has(packageName)) {
            return;
        }

        // 正在加载中
        if (this.loadingPromises.has(packageName)) {
            return this.loadingPromises.get(packageName);
        }

        // 开始加载
        const promise = this.doLoadPackage(packageName);
        this.loadingPromises.set(packageName, promise);

        try {
            await promise;
            this.loadedPackages.add(packageName);
        } finally {
            this.loadingPromises.delete(packageName);
        }
    }

    private async doLoadPackage(packageName: string): Promise<void> {
        const pkg = this.packages.get(packageName);
        if (!pkg) {
            throw new Error(`Package not found: ${packageName}`);
        }

        // 显示加载进度
        const loadingTask = wx.showLoading({
            title: '加载中...',
            mask: true
        });

        try {
            // 加载分包
            await new Promise<void>((resolve, reject) => {
                wx.loadSubpackage({
                    name: pkg.name,
                    success: () => resolve(),
                    fail: (err) => reject(err)
                });
            });

            // 预加载分包内资源
            await this.preloadPackageResources(pkg);

        } finally {
            wx.hideLoading();
        }
    }

    private calculatePriority(config: SubPackageConfig): number {
        let priority = 0;

        // 主关卡包优先级高
        if (config.name.includes('level')) {
            priority += 10;
        }

        // 独立分包优先级高
        if (config.independent) {
            priority += 5;
        }

        return priority;
    }

    // 预测性预加载
    async predictivePreload(currentScene: string): Promise<void> {
        const nextPackages = this.predictNextPackages(currentScene);

        for (const pkg of nextPackages) {
            // 后台预加载,不阻塞
            this.loadPackage(pkg.name).catch(err => {
                console.warn(`Preload failed: ${pkg.name}`, err);
            });
        }
    }

    private predictNextPackages(currentScene: string): SubPackageInfo[] {
        // 基于场景图预测下一个可能需要的分包
        const transitions = this.sceneTransitions.get(currentScene) || [];

        return transitions
            .map(scene => this.getPackageForScene(scene))
            .filter(Boolean)
            .sort((a, b) => b.priority - a.priority);
    }
}

// 资源管理器
class ResourceManager {
    private cache = new Map<string, any>();
    private references = new Map<string, number>();
    remoteBaseUrl: string;

    constructor() {
        // CDN 地址
        this.remoteBaseUrl = 'https://cdn.example.com/game-assets/';
    }

    async load(url: string): Promise<any> {
        // 检查缓存
        if (this.cache.has(url)) {
            this.references.set(url, (this.references.get(url) || 0) + 1);
            return this.cache.get(url);
        }

        // 判断是否为远程资源
        const isRemote = this.isRemoteResource(url);

        if (isRemote) {
            return await this.loadRemoteResource(url);
        } else {
            return await this.loadLocalResource(url);
        }
    }

    private async loadRemoteResource(url: string): Promise<any> {
        // 下载到本地
        const tempUrl = `${wx.env.USER_DATA_PATH}/${this.getFileName(url)}`;

        await new Promise<void>((resolve, reject) => {
            wx.downloadFile({
                url: this.remoteBaseUrl + url,
                filePath: tempUrl,
                success: () => resolve(),
                fail: reject
            });
        });

        // 加载本地资源
        return await this.loadLocalResource(tempUrl);
    }

    release(url: string): void {
        const refs = this.references.get(url) || 0;

        if (refs <= 1) {
            // 释放资源
            const asset = this.cache.get(url);
            if (asset && asset.destroy) {
                asset.destroy();
            }

            this.cache.delete(url);
            this.references.delete(url);
        } else {
            this.references.set(url, refs - 1);
        }
    }
}

2. 纹理压缩与优化

 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
51
52
53
54
55
56
// 纹理管理器
class TextureManager {
    private atlasMap = new Map<string, cc.SpriteAtlas>();
    private textureCache = new Map<string, cc.Texture2D>();

    // 创建合图
    async createAtlas(name: string, textures: string[]): Promise<cc.SpriteAtlas> {
        const atlas = new cc.SpriteAtlas();

        // 使用 TexturePacker 生成合图
        const packConfig = {
            frames: textures.map(t => ({
                filename: t,
                file: `assets/textures/${t}.png`
            })),
            metadata: {
                size: { w: 2048, h: 2048 },
                format: 'RGBA8888'
            }
        };

        // 生成合图数据
        const atlasData = await this.generateAtlas(packConfig);

        atlas.initWithJson(atlasData);
        this.atlasMap.set(name, atlas);

        return atlas;
    }

    // 纹理压缩
    async compressTexture(texture: cc.Texture2D): Promise<cc.Texture2D> {
        // 在 iOS 上使用 PVRTC
        if (cc.sys.os === cc.sys.OS_IOS) {
            return await this.compressPVRTC(texture);
        }

        // 在 Android 上使用 ETC2
        if (cc.sys.os === cc.sys.OS_ANDROID) {
            return await this.compressETC2(texture);
        }

        return texture;
    }

    // Mipmap 生成
    generateMipmaps(texture: cc.Texture2D): void {
        const size = texture.width;
        let level = 0;

        while (size >> level > 1) {
            texture.generateMipmaps(level);
            level++;
        }
    }
}

性能优化框架

1. 对象池系统

  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
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
// 对象池
class ObjectPool<T> {
    private pool: T[] = [];
    private factory: () => T;
    private reset: (obj: T) => void;
    private maxSize: number;

    constructor(
        factory: () => T,
        reset: (obj: T) => void,
        initialSize: number = 10,
        maxSize: number = 100
    ) {
        this.factory = factory;
        this.reset = reset;
        this.maxSize = maxSize;

        // 预创建对象
        for (let i = 0; i < initialSize; i++) {
            this.pool.push(factory());
        }
    }

    acquire(): T {
        if (this.pool.length > 0) {
            return this.pool.pop()!;
        }
        return this.factory();
    }

    release(obj: T): void {
        if (this.pool.length < this.maxSize) {
            this.reset(obj);
            this.pool.push(obj);
        }
    }

    resize(size: number): void {
        while (this.pool.length < size) {
            this.pool.push(this.factory());
        }

        while (this.pool.length > size) {
            this.pool.pop();
        }
    }
}

// 对象池管理器
class PoolManager {
    private pools = new Map<string, ObjectPool<any>>();

    register<T>(
        name: string,
        factory: () => T,
        reset: (obj: T) => void,
        config?: PoolConfig
    ): void {
        this.pools.set(name, new ObjectPool(
            factory,
            reset,
            config?.initialSize,
            config?.maxSize
        ));
    }

    acquire<T>(name: string): T {
        const pool = this.pools.get(name);
        if (!pool) {
            throw new Error(`Pool not found: ${name}`);
        }
        return pool.acquire();
    }

    release<T>(name: string, obj: T): void {
        const pool = this.pools.get(name);
        if (pool) {
            pool.release(obj);
        }
    }
}

// 使用示例
// 初始化
const poolManager = new PoolManager();

poolManager.register('bullet',
    () => new Bullet(),
    (bullet) => bullet.reset(),
    { initialSize: 50, maxSize: 200 }
);

poolManager.register('enemy',
    () => new Enemy(),
    (enemy) => enemy.reset(),
    { initialSize: 20, maxSize: 100 }
);

// 游戏中
function spawnBullet(position: cc.Vec3) {
    const bullet = poolManager.acquire<Bullet>('bullet');
    bullet.activate(position);
}

function destroyBullet(bullet: Bullet) {
    poolManager.release('bullet', bullet);
}

2. 渲染优化

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 批渲染管理器
class BatchRenderer {
    private batches = new Map<string, cc.Node[]>();

    addNode(node: cc.Node, batchKey: string): void {
        if (!this.batches.has(batchKey)) {
            this.batches.set(batchKey, []);
        }

        this.batches.get(batchKey)!.push(node);
    }

    render(): void {
        for (const [key, nodes] of this.batches) {
            // 相同材质的节点批量渲染
            this.renderBatch(nodes);
        }
    }

    private renderBatch(nodes: cc.Node[]): void {
        // 合并渲染调用
        const material = nodes[0].getComponent(cc.Sprite)!.getMaterial(0);

        // 设置材质
        material.update();

        // 批量绘制
        nodes.forEach(node => {
            node.getComponent(cc.Sprite)!.updateMaterial();
        });
    }
}

// 自动图集
class AutoAtlas {
    private dynamicTextures: cc.Texture2D[] = [];

    // 动态合图
    async packTextures(textures: cc.Texture2D[]): Promise<cc.SpriteAtlas> {
        // 使用 TexturePacker 或自研算法
        const bins = this.binPack(textures, 2048, 2048);

        // 创建合图纹理
        const atlasTexture = new cc.Texture2D();
        atlasTexture.initWithElement(bins.canvas);

        // 生成帧数据
        const frames = bins.rects.map((rect, index) => ({
            name: textures[index].name,
            rect: rect,
            offset: { x: 0, y: 0 },
            originalSize: { width: rect.width, height: rect.height }
        }));

        return cc.SpriteAtlas.createWithTexture(atlasTexture, frames);
    }

    private binPack(
        textures: cc.Texture2D[],
        width: number,
        height: number
    ): BinPackingResult {
        // 实现装箱算法(MaxRects、Shelf等)
        const packer = new MaxRectsPacker(width, height);
        return packer.pack(textures);
    }
}

热更新系统

1. 资源热更新

  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
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
// 热更新管理器
class HotUpdateManager {
    private remoteVersionUrl: string;
    private localVersionPath: string;
    private remoteManifest: GameManifest | null = null;
    private localManifest: GameManifest | null = null;

    constructor() {
        this.remoteVersionUrl = 'https://cdn.example.com/version.json';
        this.localVersionPath = `${wx.env.USER_DATA_PATH}/version.json`;
    }

    // 检查更新
    async checkUpdate(): Promise<UpdateInfo> {
        // 加载本地版本信息
        this.localManifest = await this.loadLocalManifest();

        // 获取远程版本信息
        this.remoteManifest = await this.fetchRemoteManifest();

        // 比较版本
        const needsUpdate = this.compareVersion(
            this.localManifest.version,
            this.remoteManifest.version
        );

        if (needsUpdate) {
            return {
                hasUpdate: true,
                currentVersion: this.localManifest.version,
                newVersion: this.remoteManifest.version,
                size: this.remoteManifest.totalSize,
                files: this.getChangedFiles()
            };
        }

        return { hasUpdate: false };
    }

    // 执行更新
    async update(
        onProgress: (progress: number) => void
    ): Promise<void> {
        const changedFiles = this.getChangedFiles();
        let downloaded = 0;
        const total = changedFiles.length;

        for (const file of changedFiles) {
            await this.downloadFile(file);
            downloaded++;
            onProgress(downloaded / total);
        }

        // 更新本地版本信息
        await this.saveManifest(this.remoteManifest!);

        // 重启游戏
        this.restartGame();
    }

    // 增量更新
    private getChangedFiles(): FileChange[] {
        const changes: FileChange[] = [];
        const remote = this.remoteManifest!;
        const local = this.localManifest!;

        for (const [path, remoteInfo] of Object.entries(remote.files)) {
            const localInfo = local.files[path];

            if (!localInfo || localInfo.md5 !== remoteInfo.md5) {
                changes.push({
                    path,
                    url: remote.files[path].url,
                    md5: remoteInfo.md5,
                    size: remoteInfo.size
                });
            }
        }

        return changes;
    }

    // 文件下载
    private async downloadFile(file: FileChange): Promise<void> {
        const tempPath = `${wx.env.USER_DATA_PATH}/${file.path}.tmp`;

        // 下载文件
        await new Promise<void>((resolve, reject) => {
            wx.downloadFile({
                url: file.url,
                filePath: tempPath,
                success: () => resolve(),
                fail: reject
            });
        });

        // 验证 MD5
        const isValid = await this.verifyMD5(tempPath, file.md5);

        if (!isValid) {
            wx.removeSavedFile({ filePath: tempPath });
            throw new Error(`MD5 verification failed: ${file.path}`);
        }

        // 移动到最终位置
        const finalPath = `${wx.env.USER_DATA_PATH}/${file.path}`;
        wx.getFileSystemManager().renameSync(tempPath, finalPath);
    }
}

总结

小游戏架构设计的核心要点:

  1. 引擎选择:根据团队技术栈选择合适的引擎
  2. 模块化设计:清晰的模块边界和依赖关系
  3. 资源管理:分包加载 + CDN + 缓存策略
  4. 性能优化:对象池、批渲染、纹理压缩
  5. 热更新:支持快速修复和内容更新

构建高质量小游戏,需要在性能、包体积和开发效率之间找到最佳平衡。


相关工具: