【Laya】LocalStorage 本地存储

Laya.LocalStorage 本地存储使用指南

简介

Laya.LocalStorage 是 LayaAir 引擎提供的本地存储管理类,用于实现数据的持久化存储。基于浏览器 LocalStorage API 封装,支持字符串和 JSON 对象的存储。

适用场景

  • 游戏存档、进度保存
  • 玩家设置、配置项存储
  • 离线数据缓存
  • 用户偏好记录

工作原理

复制代码
数据 → 序列化 → 浏览器 LocalStorage → 持久保存 → 下次启动读取

核心优势

优势 说明
数据持久化 浏览器关闭后数据仍保留
容量充足 通常约 5MB 存储空间
API 简洁 字符串和 JSON 对象分别处理
跨会话 同一域名下数据共享

目录


API 参考

属性

属性 类型 说明
count number 获取本地存储中存储的项目数量(只读)

字符串存储

方法 参数 返回值 说明
setItem(key: string, value: string) 键名, 键值 void 存储字符串类型的键值对
getItem(key: string) 键名 `string null`

JSON 存储

方法 参数 返回值 说明
setJSON(key: string, value: any) 键名, 任意值 void 存储 Object 类型(自动序列化为 JSON)
getJSON(key: string) 键名 any 获取指定键名的 Object 值(自动反序列化)

数据管理

方法 参数 返回值 说明
removeItem(key: string) 键名 void 删除指定键名的数据
clear() void 清除所有本地存储的数据

基础用法

1. 存储和读取字符串

最基础的存储方式:

typescript 复制代码
// 存储数据
Laya.LocalStorage.setItem("username", "player1");
Laya.LocalStorage.setItem("level", "10");
Laya.LocalStorage.setItem("music", "0.8");

// 读取数据
let name = Laya.LocalStorage.getItem("username");  // "player1"
let level = Laya.LocalStorage.getItem("level");    // "10"
let music = Laya.LocalStorage.getItem("music");    // "0.8"

// 读取不存在的键返回 null
let unknown = Laya.LocalStorage.getItem("unknown"); // null

2. 存储和读取 JSON 对象

存储复杂对象时使用 setJSON/getJSON

typescript 复制代码
// 存储 JSON 对象
Laya.LocalStorage.setJSON("playerData", {
    level: 25,
    score: 15000,
    inventory: ["sword", "shield", "potion"],
    stats: {
        hp: 100,
        mp: 50,
        attack: 35
    }
});

// 读取 JSON 对象
let playerData = Laya.LocalStorage.getJSON("playerData");
console.log(playerData.level);          // 25
console.log(playerData.inventory);      // ["sword", "shield", "potion"]
console.log(playerData.stats.hp);       // 100

3. 删除单个数据

typescript 复制代码
// 删除指定键的数据
Laya.LocalStorage.removeItem("username");

// 验证删除
console.log(Laya.LocalStorage.getItem("username")); // null

4. 清空所有数据

typescript 复制代码
// 清空所有存储
Laya.LocalStorage.clear();

// 检查数量
console.log(Laya.LocalStorage.count); // 0

5. 检查存储数量

typescript 复制代码
// 存储一些数据
Laya.LocalStorage.setItem("key1", "value1");
Laya.LocalStorage.setItem("key2", "value2");
Laya.LocalStorage.setItem("key3", "value3");

// 获取存储数量
console.log(Laya.LocalStorage.count); // 3

实用示例

示例1: 游戏存档系统

完整的存档管理器,支持保存/读取/删除存档:

typescript 复制代码
@regClass()
export class SaveManager {
    private static readonly SAVE_KEY = "game_save_data";
    private static readonly MAX_SAVES = 3;

    /**
     * 保存当前游戏进度
     */
    public static save(slot: number = 1): boolean {
        if (slot < 1 || slot > this.MAX_SAVES) {
            console.error("存档槽位无效:", slot);
            return false;
        }

        const saveData = {
            slot: slot,
            timestamp: Date.now(),
            version: "1.0.0",
            player: {
                level: GameManager.playerLevel,
                exp: GameManager.playerExp,
                gold: GameManager.playerGold,
                hp: GameManager.playerHp,
                maxHp: GameManager.playerMaxHp
            },
            scene: GameManager.currentScene,
            quests: GameManager.quests.map(q => ({
                id: q.id,
                status: q.status,
                progress: q.progress
            })),
            settings: {
                musicVolume: AudioManager.musicVolume,
                soundVolume: AudioManager.soundVolume
            }
        };

        const key = `${this.SAVE_KEY}_${slot}`;
        Laya.LocalStorage.setJSON(key, saveData);
        console.log(`存档已保存到槽位 ${slot}`);
        return true;
    }

    /**
     * 加载指定槽位的存档
     */
    public static load(slot: number = 1): boolean {
        if (slot < 1 || slot > this.MAX_SAVES) {
            console.error("存档槽位无效:", slot);
            return false;
        }

        const key = `${this.SAVE_KEY}_${slot}`;
        const saveData = Laya.LocalStorage.getJSON(key);

        if (!saveData) {
            console.log(`槽位 ${slot} 没有存档`);
            return false;
        }

        // 恢复游戏数据
        GameManager.playerLevel = saveData.player.level;
        GameManager.playerExp = saveData.player.exp;
        GameManager.playerGold = saveData.player.gold;
        GameManager.playerHp = saveData.player.hp;
        GameManager.playerMaxHp = saveData.player.maxHp;
        GameManager.currentScene = saveData.scene;

        // 恢复任务
        GameManager.loadQuests(saveData.quests);

        // 恢复设置
        AudioManager.musicVolume = saveData.settings.musicVolume;
        AudioManager.soundVolume = saveData.settings.soundVolume;

        console.log(`存档已加载,保存时间:${new Date(saveData.timestamp).toLocaleString()}`);
        return true;
    }

    /**
     * 检查指定槽位是否有存档
     */
    public static hasSave(slot: number = 1): boolean {
        const key = `${this.SAVE_KEY}_${slot}`;
        return Laya.LocalStorage.getItem(key) !== null;
    }

    /**
     * 获取存档信息(用于显示存档列表)
     */
    public static getSaveInfo(slot: number = 1): any | null {
        const key = `${this.SAVE_KEY}_${slot}`;
        const saveData = Laya.LocalStorage.getJSON(key);

        if (!saveData) return null;

        return {
            slot: slot,
            timestamp: saveData.timestamp,
            level: saveData.player.level,
            scene: saveData.scene
        };
    }

    /**
     * 删除指定槽位的存档
     */
    public static deleteSave(slot: number = 1): void {
        const key = `${this.SAVE_KEY}_${slot}`;
        Laya.LocalStorage.removeItem(key);
        console.log(`槽位 ${slot} 的存档已删除`);
    }

    /**
     * 获取所有存档信息
     */
    public static getAllSaves(): any[] {
        const saves = [];
        for (let i = 1; i <= this.MAX_SAVES; i++) {
            const info = this.getSaveInfo(i);
            if (info) {
                saves.push(info);
            }
        }
        return saves;
    }
}

// 使用示例
// 保存游戏
SaveManager.save(1);

// 检查是否有存档
if (SaveManager.hasSave(1)) {
    // 加载存档
    SaveManager.load(1);
}

// 获取所有存档信息
const saves = SaveManager.getAllSaves();
console.log(saves);

示例2: 游戏设置管理

设置数据的持久化存储:

typescript 复制代码
@regClass()
export class GameSettings {
    private static readonly SETTINGS_KEY = "game_settings";

    // 默认设置
    public static readonly DEFAULTS = {
        musicVolume: 0.8,
        soundVolume: 1.0,
        enableVibration: true,
        enableParticles: true,
        language: "zh-CN",
        quality: "high",
        autoSave: true,
        showFps: false
    };

    // 当前设置
    static settings = { ...this.DEFAULTS };

    /**
     * 加载设置
     */
    public static load(): void {
        const saved = Laya.LocalStorage.getJSON(this.SETTINGS_KEY);
        if (saved) {
            // 合并保存的设置和默认设置(处理新增设置项)
            this.settings = { ...this.DEFAULTS, ...saved };
        }
        console.log("设置已加载:", this.settings);
    }

    /**
     * 保存设置
     */
    public static save(): void {
        Laya.LocalStorage.setJSON(this.SETTINGS_KEY, this.settings);
        console.log("设置已保存");
    }

    /**
     * 更新单个设置并保存
     */
    public static update<K extends keyof typeof GameSettings.settings>(
        key: K,
        value: typeof GameSettings.settings[K]
    ): void {
        this.settings[key] = value;
        this.save();
    }

    /**
     * 获取设置值
     */
    public static get<K extends keyof typeof GameSettings.settings>(
        key: K
    ): typeof GameSettings.settings[K] {
        return this.settings[key];
    }

    /**
     * 重置为默认设置
     */
    public static reset(): void {
        this.settings = { ...this.DEFAULTS };
        this.save();
        console.log("设置已重置");
    }

    /**
     * 应用设置到游戏
     */
    public static apply(): void {
        AudioManager.musicVolume = this.settings.musicVolume;
        AudioManager.soundVolume = this.settings.soundVolume;
        Laya.Stat.show = this.settings.showFps;
    }
}

// 游戏启动时加载设置
GameSettings.load();
GameSettings.apply();

// 修改设置
GameSettings.update("musicVolume", 0.5);
GameSettings.update("showFps", true);

示例3: 玩家数据统计

记录玩家统计数据:

typescript 复制代码
@regClass()
export class PlayerStats {
    private static readonly STATS_KEY = "player_stats";

    static stats = {
        totalPlayTime: 0,           // 总游戏时长(秒)
        totalGamesPlayed: 0,        // 游戏次数
        totalScore: 0,              // 总得分
        highestScore: 0,            // 最高分
        totalKills: 0,              // 总击杀数
        totalDeaths: 0,             // 总死亡数
        achievements: [] as string[], // 成就列表
        firstPlayTime: 0,           // 首次游戏时间
        lastPlayTime: 0             // 最后游戏时间
    };

    /**
     * 加载统计数据
     */
    public static load(): void {
        const saved = Laya.LocalStorage.getJSON(this.STATS_KEY);
        if (saved) {
            this.stats = { ...this.stats, ...saved };
        } else {
            // 首次游戏,记录时间
            this.stats.firstPlayTime = Date.now();
        }
        this.stats.lastPlayTime = Date.now();
    }

    /**
     * 保存统计数据
     */
    public static save(): void {
        Laya.LocalStorage.setJSON(this.STATS_KEY, this.stats);
    }

    /**
     * 记录游戏结束
     */
    public static recordGameEnd(score: number, kills: number, deaths: number): void {
        this.stats.totalGamesPlayed++;
        this.stats.totalScore += score;
        this.stats.totalKills += kills;
        this.stats.totalDeaths += deaths;

        if (score > this.stats.highestScore) {
            this.stats.highestScore = score;
        }

        this.save();
    }

    /**
     * 解锁成就
     */
    public static unlockAchievement(achievementId: string): void {
        if (!this.stats.achievements.includes(achievementId)) {
            this.stats.achievements.push(achievementId);
            this.save();
            console.log("成就解锁:", achievementId);
        }
    }

    /**
     * 获取统计信息
     */
    public static getStats(): typeof PlayerStats.stats {
        return { ...this.stats };
    }

    /**
     * 计算平均得分
     */
    public static getAverageScore(): number {
        if (this.stats.totalGamesPlayed === 0) return 0;
        return Math.floor(this.stats.totalScore / this.stats.totalGamesPlayed);
    }

    /**
     * 计算 KDA
     */
    public static getKDA(): number {
        if (this.stats.totalDeaths === 0) return this.stats.totalKills;
        return parseFloat((this.stats.totalKills / this.stats.totalDeaths).toFixed(2));
    }
}

示例4: 离线缓存系统

缓存游戏资源数据:

typescript 复制代码
@regClass()
export class CacheManager {
    private static readonly CACHE_PREFIX = "cache_";
    private static readonly CACHE_META = "cache_meta";
    private static readonly DEFAULT_EXPIRE = 24 * 60 * 60 * 1000; // 24小时

    /**
     * 存储缓存数据(带过期时间)
     */
    public static set(key: string, data: any, expireMs: number = this.DEFAULT_EXPIRE): void {
        const cacheKey = this.CACHE_PREFIX + key;
        const expireTime = Date.now() + expireMs;

        const cacheData = {
            data: data,
            expire: expireTime
        };

        Laya.LocalStorage.setJSON(cacheKey, cacheData);
        this.updateMeta(key, expireTime);
    }

    /**
     * 获取缓存数据
     */
    public static get(key: string): any | null {
        const cacheKey = this.CACHE_PREFIX + key;
        const cacheData = Laya.LocalStorage.getJSON(cacheKey);

        if (!cacheData) {
            return null;
        }

        // 检查是否过期
        if (Date.now() > cacheData.expire) {
            this.remove(key);
            return null;
        }

        return cacheData.data;
    }

    /**
     * 删除缓存
     */
    public static remove(key: string): void {
        const cacheKey = this.CACHE_PREFIX + key;
        Laya.LocalStorage.removeItem(cacheKey);
    }

    /**
     * 清理所有过期缓存
     */
    public static cleanExpired(): void {
        const meta = Laya.LocalStorage.getJSON(this.CACHE_META) || {};
        const now = Date.now();
        let cleaned = 0;

        for (const key in meta) {
            if (meta[key] < now) {
                this.remove(key);
                delete meta[key];
                cleaned++;
            }
        }

        if (cleaned > 0) {
            Laya.LocalStorage.setJSON(this.CACHE_META, meta);
            console.log(`清理了 ${cleaned} 个过期缓存`);
        }
    }

    /**
     * 清空所有缓存
     */
    public static clear(): void {
        const meta = Laya.LocalStorage.getJSON(this.CACHE_META) || {};
        for (const key in meta) {
            Laya.LocalStorage.removeItem(this.CACHE_PREFIX + key);
        }
        Laya.LocalStorage.removeItem(this.CACHE_META);
    }

    /**
     * 更新缓存元数据
     */
    private static updateMeta(key: string, expireTime: number): void {
        const meta = Laya.LocalStorage.getJSON(this.CACHE_META) || {};
        meta[key] = expireTime;
        Laya.LocalStorage.setJSON(this.CACHE_META, meta);
    }
}

// 使用示例
// 缓存服务器数据,1小时过期
CacheManager.set("player_info", serverData, 60 * 60 * 1000);

// 获取缓存
const data = CacheManager.get("player_info");

// 定期清理过期缓存
setInterval(() => {
    CacheManager.cleanExpired();
}, 60 * 60 * 1000); // 每小时清理一次

示例5: 数据版本迁移

处理数据结构升级:

typescript 复制代码
@regClass()
export class MigrationManager {
    private static readonly VERSION_KEY = "data_version";
    private static readonly CURRENT_VERSION = 3;

    /**
     * 执行数据迁移
     */
    public static migrate(): void {
        const savedVersion = parseInt(Laya.LocalStorage.getItem(this.VERSION_KEY) || "0");

        if (savedVersion >= this.CURRENT_VERSION) {
            console.log("数据已是最新版本");
            return;
        }

        console.log(`开始数据迁移:${savedVersion} → ${this.CURRENT_VERSION}`);

        // 依次执行迁移
        for (let v = savedVersion + 1; v <= this.CURRENT_VERSION; v++) {
            this.runMigration(v);
        }

        // 更新版本号
        Laya.LocalStorage.setItem(this.VERSION_KEY, this.CURRENT_VERSION.toString());
        console.log("数据迁移完成");
    }

    /**
     * 执行指定版本的迁移
     */
    private static runMigration(version: number): void {
        console.log(`执行迁移 v${version}`);

        switch (version) {
            case 1:
                this.migrateToV1();
                break;
            case 2:
                this.migrateToV2();
                break;
            case 3:
                this.migrateToV3();
                break;
        }
    }

    /**
     * 迁移到版本1:重命名键名
     */
    private static migrateToV1(): void {
        const oldData = Laya.LocalStorage.getJSON("player");
        if (oldData) {
            Laya.LocalStorage.setJSON("player_data", oldData);
            Laya.LocalStorage.removeItem("player");
        }
    }

    /**
     * 迁移到版本2:添加新字段
     */
    private static migrateToV2(): void {
        const settings = Laya.LocalStorage.getJSON("game_settings");
        if (settings && !settings.quality) {
            settings.quality = "high";
            Laya.LocalStorage.setJSON("game_settings", settings);
        }
    }

    /**
     * 迁移到版本3:重构数据结构
     */
    private static migrateToV3(): void {
        const oldSaves = Laya.LocalStorage.getJSON("game_save_data");
        if (oldSaves) {
            const newSaves = {
                slot: 1,
                timestamp: oldSaves.timestamp || Date.now(),
                player: {
                    level: oldSaves.level || 1,
                    exp: oldSaves.exp || 0
                }
            };
            Laya.LocalStorage.setJSON("game_save_data_1", newSaves);
            Laya.LocalStorage.removeItem("game_save_data");
        }
    }
}

// 游戏启动时执行迁移
MigrationManager.migrate();

高级技巧

1. 数据加密存储

敏感数据加密后存储:

typescript 复制代码
@regClass()
export class SecureStorage {
    // 简单的 XOR 加密(生产环境应使用更安全的加密方式)
    private static encrypt(data: string, key: string = "layaair"): string {
        let result = "";
        for (let i = 0; i < data.length; i++) {
            result += String.fromCharCode(
                data.charCodeAt(i) ^ key.charCodeAt(i % key.length)
            );
        }
        return btoa(result); // Base64 编码
    }

    private static decrypt(data: string, key: string = "layaair"): string {
        const decoded = atob(data); // Base64 解码
        let result = "";
        for (let i = 0; i < decoded.length; i++) {
            result += String.fromCharCode(
                decoded.charCodeAt(i) ^ key.charCodeAt(i % key.length)
            );
        }
        return result;
    }

    public static setItem(key: string, value: string): void {
        const encrypted = this.encrypt(value);
        Laya.LocalStorage.setItem("sec_" + key, encrypted);
    }

    public static getItem(key: string): string | null {
        const encrypted = Laya.LocalStorage.getItem("sec_" + key);
        if (!encrypted) return null;
        return this.decrypt(encrypted);
    }
}

// 使用
SecureStorage.setItem("token", "player_secret_token");
const token = SecureStorage.getItem("token");

2. 数据压缩存储

使用 JSON 简化减少存储空间:

typescript 复制代码
@regClass()
export class CompressedStorage {
    /**
     * 存储时只保留必要字段
     */
    public static setPlayerData(data: any): void {
        const compressed = {
            l: data.level,      // level -> l
            e: data.exp,        // exp -> e
            g: data.gold,       // gold -> g
            i: data.inventory   // inventory -> i
        };
        Laya.LocalStorage.setJSON("p", compressed);
    }

    /**
     * 读取时恢复完整字段名
     */
    public static getPlayerData(): any | null {
        const compressed = Laya.LocalStorage.getJSON("p");
        if (!compressed) return null;

        return {
            level: compressed.l,
            exp: compressed.e,
            gold: compressed.g,
            inventory: compressed.i
        };
    }
}

3. 使用常量定义键名

避免字符串拼写错误:

typescript 复制代码
export class StorageKeys {
    // 存档相关
    public static readonly SAVE_DATA = "game_save_data";
    public static readonly AUTO_SAVE = "auto_save";

    // 设置相关
    public static readonly SETTINGS = "game_settings";
    public static readonly AUDIO_SETTINGS = "audio_settings";

    // 玩家相关
    public static readonly PLAYER_DATA = "player_data";
    public static readonly PLAYER_STATS = "player_stats";

    // 缓存相关
    public static readonly CACHE_PREFIX = "cache_";
}

// 使用
Laya.LocalStorage.setJSON(StorageKeys.SAVE_DATA, saveData);
Laya.LocalStorage.setItem(StorageKeys.SETTINGS, JSON.stringify(settings));

4. 存储空间检测

检测存储空间是否充足:

typescript 复制代码
@regClass()
export class StorageChecker {
    /**
     * 检查存储空间
     */
    public static checkSpace(): { used: number; remaining: number; percent: number } {
        let used = 0;
        for (let i = 0; i < Laya.LocalStorage.count; i++) {
            const key = localStorage.key(i);
            if (key) {
                used += localStorage.getItem(key)?.length || 0;
            }
        }

        const total = 5 * 1024 * 1024; // 约 5MB
        const remaining = total - used;
        const percent = Math.floor((used / total) * 100);

        return { used, remaining, percent };
    }

    /**
     * 检查是否能存储数据
     */
    public static canStore(data: any): boolean {
        const json = JSON.stringify(data);
        const space = this.checkSpace();
        return json.length < space.remaining;
    }

    /**
     * 显示存储信息
     */
    public static logInfo(): void {
        const space = this.checkSpace();
        console.log(`存储使用情况: ${space.percent}%`);
        console.log(`已使用: ${(space.used / 1024).toFixed(2)} KB`);
        console.log(`剩余: ${(space.remaining / 1024).toFixed(2)} KB`);
    }
}

5. 批量操作

typescript 复制代码
@regClass()
export class BatchStorage {
    /**
     * 批量存储
     */
    public static setItems(items: Record<string, string>): void {
        for (const key in items) {
            Laya.LocalStorage.setItem(key, items[key]);
        }
    }

    /**
     * 批量获取
     */
    public static getItems(keys: string[]): Record<string, string | null> {
        const result: Record<string, string | null> = {};
        for (const key of keys) {
            result[key] = Laya.LocalStorage.getItem(key);
        }
        return result;
    }

    /**
     * 批量删除
     */
    public static removeItems(keys: string[]): void {
        for (const key of keys) {
            Laya.LocalStorage.removeItem(key);
        }
    }

    /**
     * 按前缀删除
     */
    public static removeByPrefix(prefix: string): void {
        const keys: string[] = [];
        for (let i = 0; i < Laya.LocalStorage.count; i++) {
            const key = localStorage.key(i);
            if (key && key.startsWith(prefix)) {
                keys.push(key);
            }
        }
        this.removeItems(keys);
    }
}

// 使用
BatchStorage.setItems({
    "key1": "value1",
    "key2": "value2",
    "key3": "value3"
});

BatchStorage.removeByPrefix("cache_");

最佳实践

1. 合理规划数据结构

建议 说明
使用 JSON 存储复杂对象 setJSON/getJSON 处理对象
键名使用前缀分类 save_, settings_, cache_
避免存储大文件 图片、音频等不应存入 LocalStorage
定期清理过期数据 实现缓存过期机制

2. 错误处理

typescript 复制代码
// ✅ 正确:添加错误处理
public static safeSetJSON(key: string, value: any): boolean {
    try {
        Laya.LocalStorage.setJSON(key, value);
        return true;
    } catch (e) {
        console.error("存储失败:", e);
        // 可能是空间不足或隐私模式
        return false;
    }
}

public static safeGetJSON<T>(key: string): T | null {
    try {
        return Laya.LocalStorage.getJSON(key) as T;
    } catch (e) {
        console.error("读取失败:", e);
        return null;
    }
}

3. 版本控制

typescript 复制代码
// ✅ 正确:为数据添加版本信息
interface SaveData {
    version: string;
    timestamp: number;
    player: PlayerData;
}

const saveData: SaveData = {
    version: "1.0.0",
    timestamp: Date.now(),
    player: { ... }
};

4. 数据验证

typescript 复制代码
// ✅ 正确:验证读取的数据
public static loadPlayerData(): PlayerData | null {
    const data = Laya.LocalStorage.getJSON("player_data");

    // 验证数据完整性
    if (!data || typeof data !== "object") return null;
    if (!data.level || typeof data.level !== "number") return null;
    if (!data.inventory || !Array.isArray(data.inventory)) return null;

    return data;
}

5. 默认值处理

typescript 复制代码
// ✅ 正确:提供默认值
export class GameSettings {
    private static readonly DEFAULTS = {
        musicVolume: 0.8,
        soundVolume: 1.0
    };

    public static getMusicVolume(): number {
        const saved = Laya.LocalStorage.getItem("musicVolume");
        return saved ? parseFloat(saved) : this.DEFAULTS.musicVolume;
    }
}

注意事项

  1. 容量限制:浏览器 LocalStorage 通常限制约 5MB,超出会抛出异常
  2. 同步操作:存储/读取是同步的,大量数据可能阻塞主线程
  3. 隐私模式:浏览器隐私/无痕模式下可能无法使用
  4. 字符串存储setItem/getItem 只能存储字符串,数值需自行转换
  5. 跨域隔离:不同域名/端口/协议的存储相互独立
  6. 数据安全:存储的数据明文可见,敏感信息需加密
  7. 兼容性:部分浏览器在禁用 Cookie 时也会禁用 LocalStorage

相关文档

相关推荐
We་ct7 小时前
LeetCode 42. 接雨水:双指针解法深度剖析与全方法汇总
前端·算法·leetcode·typescript
怣疯knight9 小时前
外部类触发角色状态切换
游戏程序
妙为10 小时前
unreal engine5角色把敌人 “挤飞”
游戏引擎·虚幻·ue·unrealengine5
两水先木示10 小时前
【Unity】对指定物体进行描边——模板测试法
unity·游戏引擎·shader·外描边
冲刺逆向11 小时前
【js逆向案例五】瑞数通杀模版
前端·javascript·typescript
weixin_4242946711 小时前
Unity项目的Artifacts文件夹过大怎么解决?
unity·游戏引擎
wuhen_n1 天前
TypeScript的对象类型:interface vs type
前端·javascript·typescript
EndingCoder1 天前
反射和元数据:高级装饰器用法
linux·运维·前端·ubuntu·typescript
syker1 天前
3D游戏引擎Bluely Engine 开发手册
开发语言·3d·游戏引擎