单机游戏数据的自动保存方案
大家好,2023年还有最后的3天!
有小伙伴私信我,说:
总感觉一股脑的全盘定时保存不科学,也写过保存变化的玩家数据,但是改完数据就得手动标记一下字段变化,感觉不够智能,不知道有没好的设计模式之类可以解决,就只管更新数据就行。
笔者认真思考了一下,结合前面的项目里面用到的,给大家分析一下,大家可以根据具体情况看看。
本文将介绍一下单机游戏数据的自动保存方案。
本文源工程可在文末阅读原文获取,小伙伴们自行前往。
根据小伙伴的私信,需求如下:
我们接下来具体分析一下
其实全盘定时保存也不是不好的设计方案,只是他也是需要针对变化的数据进行存盘。
也就是说我们需要对变化的数据进行标记,在执行全盘保存的时候,需要根据标记位来进行存储。
这方案在后端开发其实是很常见的,定时存盘+离线存盘。
能够有效地保证数据存储无误、存储效率更高,服务器宕机时损失最少。
手动标记其实是最有效,最直接的去控制指定内容是否需要存盘的方法。
但是由于是手动标记,也就是人为操作,难免会出现错漏的情况。
因此可以借助一下设计模式,去优化一下设计,在数据变化时可以自动标记。
下面一起来看下自动保存常用设计模式
下面是查阅相关资料之后整理出来的一些设计模式和方法:
观察者模式: 使用观察者模式来监测游戏中的变化。每个可能修改数据的对象都是观察者,而存档系统是主题。当对象发生变化时,它通知主题,主题再负责触发保存。
Dirty Flag模式: 引入“脏标志”来标记对象是否发生变化。只有在对象发生变化时才进行保存。这种方式可以减少不必要的保存操作。
快照模式: 定期创建游戏状态的快照,而不是全盘保存。这样可以避免频繁的保存操作,只在需要时加载最近的快照。
增量保存: 只保存发生变化的部分数据,而不是整个数据集。这可以减少保存和加载的时间,尤其是在数据量较大的情况下。
接下来直接看下实例
我们使用观察者模式来完成一个数据自动保存的实例。
首先我们准备一下玩家数据,其中包括:
// PlayerData.ts
export class PlayerData {
name: string;
level: number;
constructor(data: any) {
this.name = data.name;
this.level = data.level;
}
}
然后,我们定义一个通用的观察者接口:
// Observer.ts
export interface Observer<T> {
update(data: T): void;
}
再然后,实现一个具体的观察者,即自动保存数据的观察者,核心内容如下:
save
负责存储数据,这里可以根据具体需求存本地或者服务器。load
负责数据加载。import { sys } from "cc";
import { Observer } from "./Observer";
import { PlayerData } from "./PlayerData";
// AutoSaveObserver.ts
export class AutoSaveObserver implements Observer<PlayerData> {
update(data: PlayerData): void {
// 在这里执行自动保存操作,可以调用存档系统
console.log(`Auto-saving data: ${JSON.stringify(data)}`);
this.save(data);
}
save(data: PlayerData) {
sys.localStorage.setItem('playerData', JSON.stringify(data));
}
load() {
const data = sys.localStorage.getItem('playerData');
if (data) {
const parsedData = JSON.parse(data);
return new PlayerData(parsedData);
}
return new PlayerData({ name: "Player", level: 1 });
}
}
再再然后,我们创建一个通用的主题类,使用代理模式来处理观察者管理和通知:
其中要实现自动标记/存盘的核心是Proxy:
在 TypeScript 中,Proxy 是 ES6 引入的一种特性,它提供了一种拦截、定义自定义行为的机制。Proxy 可以用于创建一个代理对象,该对象可以拦截对原始对象的访问、属性查找、赋值等操作。这为开发者提供了一种在对象级别上自定义行为的方式。
代码如下:
import { Observer } from "./Observer";
// ObservableProxy.ts
export class ObservableProxy<T extends object> {
private observers: Observer<T>[] = [];
private _target: T;
constructor(target: T) {
this._target = new Proxy(target, {
set: (obj, prop, value) => {
if (obj[prop] !== value) {
obj[prop] = value;
this.notifyObservers();
}
return true;
},
});
}
addObserver(observer: Observer<T>): void {
this.observers.push(observer);
}
removeObserver(observer: Observer<T>): void {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
private notifyObservers(): void {
for (const observer of this.observers) {
observer.update(this._target);
}
}
get target(): T {
return this._target;
}
}
最后我们通过ObservableProxy
对玩家数据进行包装,实现自动保存。
import { ObservableProxy } from "./ObservableProxy";
import { PlayerData } from "./PlayerData";
// ObservablePlayerData.ts
export class ObservablePlayerData extends ObservableProxy<PlayerData> {
constructor(data: any) {
super(new PlayerData(data));
}
}
import { _decorator, Component, Node } from 'cc';
import { ObservablePlayerData } from './ObservablePlayerData';
import { AutoSaveObserver } from './AutoSaveObserver';
const { ccclass, property } = _decorator;
@ccclass('Main')
export class Main extends Component {
start() {
// Main.ts
const autoSaveObserver = new AutoSaveObserver();
var playerData = autoSaveObserver.load();
const observablePlayerData = new ObservablePlayerData(playerData);
console.log(`Now data: ${JSON.stringify(observablePlayerData.target)}`);
// 将自动保存观察者添加到观察者列表中
observablePlayerData.addObserver(autoSaveObserver);
// 修改玩家数据,会触发自动保存
observablePlayerData.target.level = 2;
observablePlayerData.target.level = 3;
}
}
首次运行,初始等级1级,经过修改等级后,玩家等级是3级。
重新登录后,加载到的等级为3级,测试自动存盘成功。
把冰箱门关上!下课!
本文源工程可通过私信AutoSave获取。
在哪里可以看到如此清晰的思路,快跟上我的节奏!关注我,和我一起了解游戏行业最新动态,学习游戏开发技巧。
我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。
AD:笔者线上的小游戏《贪吃蛇掌机经典》《重力迷宫球》《填色之旅》大家可以自行点击搜索体验。
实不相瞒,想要个赞和在看!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!
推荐专栏:
100个Cocos实例
8年主程手把手打造Cocos独立游戏开发框架
和8年游戏主程一起学习设计模式
从零开始开发贪吃蛇小游戏到上线系列
知识付费专栏