Skip to content
游戏存档

游戏存档

目前闯关小游戏只是对局内的游戏进度通过检查点进行了保存,但是当我们重新进入游戏,就需要从第一关重新开始。我们这节课就需要使用编辑器提供的数据存储功能来将我们的游戏数据进行持久化存储。(本节课使用数据中心来实现数据存储)

存档逻辑简述如下:

每个玩家都有一个数字来代表存档,初始值为0,每当角色进入新的关卡时,就会刷新这个数字,并将这个数字进行永久存储。玩家下一次上线时就会读取这个数字,并通过这个数字将角色传送到对应的关卡。

1.创建数据体脚本

使用数据中心来存储数据,我们首先需要一个数据体脚本,数据体脚本是用来确定我们数据有哪些内容,以及数据初始化、数据更新,保存等逻辑。

创建LevelData脚本

①点击“新建脚本”。

②将脚本命名为LevelData,该脚本就是我们的关卡数据数据体脚本。

image-20230830181907758

编辑LevelData脚本

该脚本有如下几个要点:

数据体脚本必须继承Subdata,否侧数据体不能够被数据中心进行存储。

只有给字段添加装饰器 @Decorator.persistence() 数据才能够被永久存储。

initDefaultData是Subdata提供的函数,用来初始化数据。(没有存档就会调用这个函数)

ts
export class LevelData extends Subdata{

    @Decorator.persistence()
    pointNumber:number

    protected initDefaultData(): void {
        this.pointNumber = 0
    }
}
export class LevelData extends Subdata{

    @Decorator.persistence()
    pointNumber:number

    protected initDefaultData(): void {
        this.pointNumber = 0
    }
}

2.在服务端开启数据存储监听

数据存储有一个前提,就是必须在服务端才能够进行存储。由于经过检查点是在客户端进行的逻辑判断,客户端是不能够进行数据存储的,所以在后面我们需要通过让客户端发送事件到服务端,来让服务端帮助完成数据存储。在让客户端发送事件之前,我们需要在服务端开启对应的监听事件。

目前我们的GameStart脚本是在客户端和服务端都能够执行的,所以在GameStart中添加逻辑。

本此添加的主要内容:

在服务端开启了对事件"SavePoint"的监听。

监听事件接收一个pointNumber参数,该参数是客户端发送过来的关卡号。

使用DataCenterS获取到玩家的LevelData数据

获取到数据后,将客户端传递过来的参数进行保存

ts
import { LevelData } from "./LevelData";
import { LevelManager } from "./LevelManager";
import { HelperUI } from "./UI/HelperUI";

@Component
export default class GameStart extends Script {

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected async onStart(): Promise<void> {

        if(SystemUtil.isClient()){
            LevelManager.instance.init()

            UIService.show(HelperUI);            
        }

        if(SystemUtil.isServer()){ 
            // 数据存储逻辑 
            Event.addClientListener("SavePoint",(player:Player,pointNumber:number)=>{ 
                // 使用数据中心存储数据 
 
                // 改变数据 
                DataCenterS.getData(player,LevelData).pointNumber = pointNumber 
 
                // 存储数据 
                DataCenterS.getData(player,LevelData).save(true) 
            }) 
        } 
    }

}
import { LevelData } from "./LevelData";
import { LevelManager } from "./LevelManager";
import { HelperUI } from "./UI/HelperUI";

@Component
export default class GameStart extends Script {

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected async onStart(): Promise<void> {

        if(SystemUtil.isClient()){
            LevelManager.instance.init()

            UIService.show(HelperUI);            
        }

        if(SystemUtil.isServer()){ 
            // 数据存储逻辑 
            Event.addClientListener("SavePoint",(player:Player,pointNumber:number)=>{ 
                // 使用数据中心存储数据 
 
                // 改变数据 
                DataCenterS.getData(player,LevelData).pointNumber = pointNumber 
 
                // 存储数据 
                DataCenterS.getData(player,LevelData).save(true) 
            }) 
        } 
    }

}

3.在客户端发送数据存储事件

上一步我们在服务端添加了数据存储的事件监听,这一步我们就需要判断当角色进入新检查点的时候,向服务端发送事件以存储数据。

LevelManager脚本中添加如下逻辑:

通过lastPointNumber与此次的检查点序号进行比较,判断出是否为新关卡。

假如为新关卡,就让客户端向服务端发送"SavePoint"事件,并将关卡序号传递过去。

ts
import CheckPointTrigger from "./CheckPointTrigger"
import { LevelData } from "./LevelData"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager {
	// 省略代码
    ......

    public async init() {
        this._deathTrigger = await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other: GameObject) => {
            // 当进入的物体是角色类型
            if (other instanceof Character) {
                // 让角色死亡
                this.charDeath(other)
            }
        })

        Event.addLocalListener("CheckPoint", (checkPointTrigger: CheckPointTrigger) => {

            // 当角色进入新关卡的时候,存储新关卡 
            if (checkPointTrigger.pointNumber > this.lastPointNumber) { 
                // 存储数据 
                Event.dispatchToServer("SavePoint", checkPointTrigger.pointNumber) 
            } 

            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()

            this.lastPointNumber = checkPointTrigger.pointNumber
        })
        
        // 省略代码
        ......

    }
        
	// 省略代码
	.....
}
import CheckPointTrigger from "./CheckPointTrigger"
import { LevelData } from "./LevelData"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager {
	// 省略代码
    ......

    public async init() {
        this._deathTrigger = await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other: GameObject) => {
            // 当进入的物体是角色类型
            if (other instanceof Character) {
                // 让角色死亡
                this.charDeath(other)
            }
        })

        Event.addLocalListener("CheckPoint", (checkPointTrigger: CheckPointTrigger) => {

            // 当角色进入新关卡的时候,存储新关卡 
            if (checkPointTrigger.pointNumber > this.lastPointNumber) { 
                // 存储数据 
                Event.dispatchToServer("SavePoint", checkPointTrigger.pointNumber) 
            } 

            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()

            this.lastPointNumber = checkPointTrigger.pointNumber
        })
        
        // 省略代码
        ......

    }
        
	// 省略代码
	.....
}

4.游戏开始时获取数据

在上两步,我们已经完成了存储数据的逻辑。现在我们就需要在游戏一开始的时候,获取到存储的数据并进行跳关。

该逻辑还是需要在LevelManager脚本中进行执行:

此次添加有如下几个要点:

需要等待 DataCenterC 准备结束,才能够获取到正确的数据。

获取到数据之后,调用jumpToPoint进行跳关。

ts
import CheckPointTrigger from "./CheckPointTrigger"
import { LevelData } from "./LevelData"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager {
    // 单例模式
    private static _instacne: LevelManager
    public static get instance(): LevelManager {
        if (LevelManager._instacne == null) {
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger: Trigger

    /**复活位置 */
    private _rebornPosition: Vector = new Vector(10, 0, 420)

    /**所有的检查点脚本 */
    public checkPointMap: Map<number, CheckPointTrigger> = new Map()

    /**上一个检查点的序号 */
    public lastPointNumber: number = 0

    public async init() {
        this._deathTrigger = await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other: GameObject) => {
            // 当进入的物体是角色类型
            if (other instanceof Character) {
                // 让角色死亡
                this.charDeath(other)
            }
        })

        Event.addLocalListener("CheckPoint", (checkPointTrigger: CheckPointTrigger) => {

            // 当角色进入新关卡的时候,存储新关卡
            if (checkPointTrigger.pointNumber > this.lastPointNumber) {
                // 存储数据
                Event.dispatchToServer("SavePoint", checkPointTrigger.pointNumber)
            }

            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()

            this.lastPointNumber = checkPointTrigger.pointNumber
        })


        // 等待数据中心准备完毕 
        await DataCenterC.ready() 
 
        // 通过客户端数据中心,获取数据 
        let pointNumber = DataCenterC.getData(LevelData).pointNumber 
 
        UIService.show(GameUI, this.checkPointMap.size, pointNumber) 
 
        // 让角色跳关 
        this.jumpToPoint(pointNumber) 
    }

    /**让角色死亡 */
    public charDeath(char: Character) {
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421", char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if (char == Player.localPlayer.character) {
            Event.dispatchToLocal("Death")
        }
    }

    /**让角色复活 */
    public charReborn(char: Character) {
        if (char == Player.localPlayer.character) {
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }


    /**
     * 跳转到指定关卡
     * @param pointNumber 关卡号
     */
    public jumpToPoint(pointNumber: number = this.lastPointNumber) {
        // 实现跳转
        let checkPoint = this.checkPointMap.get(pointNumber)
        if (checkPoint) {
            Player.localPlayer.character.worldTransform.position = checkPoint.gameObject.worldTransform.position.clone()
        }
    }
}
import CheckPointTrigger from "./CheckPointTrigger"
import { LevelData } from "./LevelData"
import GameUI from "./UI/GameUI"

/**
 * 关卡管理器
 */
export class LevelManager {
    // 单例模式
    private static _instacne: LevelManager
    public static get instance(): LevelManager {
        if (LevelManager._instacne == null) {
            LevelManager._instacne = new LevelManager()
        }
        return LevelManager._instacne
    }

    /**死亡触发器 */
    private _deathTrigger: Trigger

    /**复活位置 */
    private _rebornPosition: Vector = new Vector(10, 0, 420)

    /**所有的检查点脚本 */
    public checkPointMap: Map<number, CheckPointTrigger> = new Map()

    /**上一个检查点的序号 */
    public lastPointNumber: number = 0

    public async init() {
        this._deathTrigger = await GameObject.asyncFindGameObjectById("299CDDA6") as Trigger
        this._deathTrigger.onEnter.add((other: GameObject) => {
            // 当进入的物体是角色类型
            if (other instanceof Character) {
                // 让角色死亡
                this.charDeath(other)
            }
        })

        Event.addLocalListener("CheckPoint", (checkPointTrigger: CheckPointTrigger) => {

            // 当角色进入新关卡的时候,存储新关卡
            if (checkPointTrigger.pointNumber > this.lastPointNumber) {
                // 存储数据
                Event.dispatchToServer("SavePoint", checkPointTrigger.pointNumber)
            }

            this._rebornPosition = checkPointTrigger.gameObject.worldTransform.position.clone()

            this.lastPointNumber = checkPointTrigger.pointNumber
        })


        // 等待数据中心准备完毕 
        await DataCenterC.ready() 
 
        // 通过客户端数据中心,获取数据 
        let pointNumber = DataCenterC.getData(LevelData).pointNumber 
 
        UIService.show(GameUI, this.checkPointMap.size, pointNumber) 
 
        // 让角色跳关 
        this.jumpToPoint(pointNumber) 
    }

    /**让角色死亡 */
    public charDeath(char: Character) {
        // 开启布娃娃属性
        char.ragdollEnabled = true
        // 播放特效
        EffectService.playAtPosition("27421", char.worldTransform.position)
        // 播放音效
        SoundService.playSound("120841")
        setTimeout(() => {
            // 让角色复活
            this.charReborn(char)
        }, 3000);

        if (char == Player.localPlayer.character) {
            Event.dispatchToLocal("Death")
        }
    }

    /**让角色复活 */
    public charReborn(char: Character) {
        if (char == Player.localPlayer.character) {
            // 将角色的位置改变到复活点
            char.worldTransform.position = this._rebornPosition.clone()
        }

        // 关闭布娃娃属性
        char.ragdollEnabled = false
    }


    /**
     * 跳转到指定关卡
     * @param pointNumber 关卡号
     */
    public jumpToPoint(pointNumber: number = this.lastPointNumber) {
        // 实现跳转
        let checkPoint = this.checkPointMap.get(pointNumber)
        if (checkPoint) {
            Player.localPlayer.character.worldTransform.position = checkPoint.gameObject.worldTransform.position.clone()
        }
    }
}

5.设置存储环境

存储环境是我们必须要进行设置的。因为游戏在PIE(编辑器运行环境)环境下,数据是没有服务器进行托管的,所以只能将数据存储在本地。然而当游戏发布到手机端之后,数据就可以由服务器进行托管,我们就可以通过设置,来让数据进行永久存储。

设置存储环境的代码:

ts
DataStorage.setTemporaryStorage(SystemUtil.isPIE)
DataStorage.setTemporaryStorage(SystemUtil.isPIE)

我们需要让这行代码在服务端进行调用,只需要调用一次即可。所以我们可以将其添加到 GameStart 脚本

ts
import { LevelData } from "./LevelData";
import { LevelManager } from "./LevelManager";
import { HelperUI } from "./UI/HelperUI";

@Component
export default class GameStart extends Script {

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected async onStart(): Promise<void> {

        if(SystemUtil.isClient()){
            LevelManager.instance.init()

            UIService.show(HelperUI);            
        }

        if(SystemUtil.isServer()){ 
            // 设置存储环境 
            DataStorage.setTemporaryStorage(SystemUtil.isPIE) 
            // 数据存储逻辑 
            Event.addClientListener("SavePoint",(player:Player,pointNumber:number)=>{ 
                // 使用数据中心存储数据 
 
                // 改变数据 
                DataCenterS.getData(player,LevelData).pointNumber = pointNumber 
 
                // 存储数据 
                DataCenterS.getData(player,LevelData).save(true) 
            }) 
        } 
    }

}
import { LevelData } from "./LevelData";
import { LevelManager } from "./LevelManager";
import { HelperUI } from "./UI/HelperUI";

@Component
export default class GameStart extends Script {

    /** 当脚本被实例后,会在第一帧更新前调用此函数 */
    protected async onStart(): Promise<void> {

        if(SystemUtil.isClient()){
            LevelManager.instance.init()

            UIService.show(HelperUI);            
        }

        if(SystemUtil.isServer()){ 
            // 设置存储环境 
            DataStorage.setTemporaryStorage(SystemUtil.isPIE) 
            // 数据存储逻辑 
            Event.addClientListener("SavePoint",(player:Player,pointNumber:number)=>{ 
                // 使用数据中心存储数据 
 
                // 改变数据 
                DataCenterS.getData(player,LevelData).pointNumber = pointNumber 
 
                // 存储数据 
                DataCenterS.getData(player,LevelData).save(true) 
            }) 
        } 
    }

}