SOURCE

console 命令行工具 X clear

                    
>
console
const demoCode = `
// const game = new Game()
const directions = ["left", "right", "up", "down"]

async function runGame() {
    for (let count = 0; count < 500; count++) {
        const direction = directions[Math.floor(Math.random() * 4)]
        console.log(direction)
        await game.move(direction)
    }
}

setInterval(() => {
    game.data.player.position.x = 7
    game.data.player.position.y = 12
}, 30000)

runGame()

// exports.gameObject = game
`

interpreter.run(demoCode)

// const game = interpreter.exports.gameObject

folderState.addMonitor(game, "name", {
    label: "游戏名称"
})
<canvas id="ok" width="375" height="625"></canvas>

<div class="description">
	<ul>
		<li>✅ 关卡数据通过 tweakpane 绑定显示</li>
		<li>✅ 关卡运行控制按钮(开始、结束、重新开始、暂停、继续运行、运行速度)</li>
		<li>✅ 关卡状态数据由一个 reactive 对象统一管理</li>
		<li>✅ 关卡状态由数据驱动运行</li>
		<li>✅ 关卡自动随机运行</li>
		<li>✅ 带有 pause/restart 功能的 dealy 函数</li>
		<li>⚪ 关卡状态可随时从给出的数据中恢复(存档)</li>
		<li></li>
	</ul>
</div>

<script>
    const LEVEL = Vue.reactive({
        autoplay: true,
        isPause: false,
        speed: 500,
        money: 0,
        player: {
            position: {
                x: 7,
                y: 12
            }
        },
        npcList: [
            { position: { x: 1, y: 1 }, colorIndex: 0 },
            { position: { x: 2, y: 2 }, colorIndex: 1 },
            { position: { x: 3, y: 3 }, colorIndex: 2 },
            { position: { x: 4, y: 4 }, colorIndex: 3 },
            { position: { x: 5, y: 5 }, colorIndex: 4 },
            { position: { x: 6, y: 6 }, colorIndex: 5 },
        ]
    })
    
    class Game {
        name = "探险家"

        zr = zrender.init(document.getElementById("ok"), {
            devicePixelRatio: 0.5,
            width: 375 * 2,
            height: 625 * 2
        })

        screenColumn = 15
        screenRow = 25
        tileSize = 50
        canvasWidth = 15 * 25 * 2
        canvasHeight = 25 * 25 * 2
        gridColor = "#CCC"
        playerColor = "#000"
        npcColors = ["#F00", "#FF0", "#0F0", "#0FF", "#F0F", "#00F"]

        gridGroup = new zrender.Group()
        npcGroup = new zrender.Group()
        player = null

        data = LEVEL

        delayCheckInterval = 200
        delayThreshold = 0
        delayCounter = 0

        constructor() {
            this.setSpeed()

            this.zr.add(this.gridGroup)
            this.zr.add(this.npcGroup)

            this.player = this.createTile(this.data.player.position, this.playerColor)
            this.zr.add(this.player)

            this.drawGrid()

            this.data.npcList.forEach(npc => {
                this.npcGroup.add(this.createTile(npc.position, this.npcColors[npc.colorIndex]))
            })

            /**
             * ✅ 实例内部修改引用的外部 reactive 可以被监测到
             */
            const that = this
            setInterval(function() {
                that.data.money++
            }, 1000)
        }

        drawGrid() {
            for (let h = 0; h <= this.screenColumn; h++) {
                const vLine = new zrender.Line({
                    shape: {
                        x1: h * this.tileSize,
                        y1: 0,
                        x2: h * this.tileSize,
                        y2: this.canvasHeight
                    },
                    silent: true
                })
                this.gridGroup.add(vLine)
            }

            for (let v = 0; v <= this.screenRow; v++) {
                const hLine = new zrender.Line({
                    shape: {
                        x1: 0,
                        y1: v * this.tileSize,
                        x2: this.canvasWidth,
                        y2: v * this.tileSize
                    },
                    silent: true
                })
                this.gridGroup.add(hLine)
            }
        }

        createTile(position = { x: 0, y: 0 }, color = this.npcColors[0]) {
            return new zrender.Rect({
                shape: {
                    x: position.x * this.tileSize,
                    y: position.y * this.tileSize,
                    width: this.tileSize,
                    height: this.tileSize
                },
                style: {
                    fill: color
                },
                silent: true
            })
        }

        pause() {
            this.data.isPause = true
        }

        resume() {
            this.data.isPause = false
        }

        setSpeed(speed) {
            if (speed) this.data.speed = speed
            this.delayThreshold = Math.floor(this.data.speed / this.delayCheckInterval)
        }
        
        checkBeforeExcute() {
            const that = this
            return new Promise((resolve, reject) => {
                console.log(that.delayCounter * that.delayCheckInterval)
                that.delayCounter++
                setTimeout(function() {
                    if (that.delayCounter > that.delayThreshold && !that.data.isPause) {
                        that.delayCounter = 0
                        resolve()
                    } else {
                        reject()
                    }
                }, this.delayCheckInterval)
            })
            .catch(err => {
                return that.checkBeforeExcute()
            })
        }

        async move(direction = "left") {
            if (this.data.speed > 0 || this.data.isPause) {
                await this.checkBeforeExcute()
            }

            switch(direction) {
                case "left":
                    if (this.data.player.position.x - 1 >= 0) this.data.player.position.x--
                    break
                case "right":
                    if (this.data.player.position.x + 1 < this.screenColumn) this.data.player.position.x++
                    break
                case "up":
                    if (this.data.player.position.y - 1 >= 0) this.data.player.position.y--
                    break
                case "down":
                    if (this.data.player.position.y + 1 < this.screenRow) this.data.player.position.y++
            }
            
            this.player.animate("shape", false)
                .when(this.data.speed, {
                    x: this.data.player.position.x * this.tileSize,
                    y: this.data.player.position.y * this.tileSize
                })
                .start()
            
            console.log(`move ${direction} ok!`)
        }
    }

    const game = new Game()

    const interpreter = new Sval({
        ecmaVer: 9,
        sandBox: true
    })

    interpreter.import("game", game)

</script>

<script>
	const pane = new Tweakpane.Pane()

    /**
     * 分组
     */
    const  folderControl = pane.addFolder({
        title: "状态控制"
    })

    const folderParam = pane.addFolder({
        title: "可调节参数"
    })

    const folderState = pane.addFolder({
        title: "状态数据"
    })

    /**
     * 状态控制按钮
     */
    const playButton = folderControl.addButton({
        title: "开始"
    })
    const pauseButton = folderControl.addButton({
        title: "暂停"
    })
    const resumeButton = folderControl.addButton({
        title: "继续",
        disabled: true
    })
    const restartButton = folderControl.addButton({
        title: "重新开始"
    })
    const stopButton = folderControl.addButton({
        title: "结束"
    })

    pauseButton.on("click", function() {
        pauseButton.disabled = true
        resumeButton.disabled = false
        LEVEL.isPause = true
        console.log("-------------> 暂停!")
    })

    resumeButton.on("click", function() {
        pauseButton.disabled = false
        resumeButton.disabled = true
        LEVEL.isPause = false
        console.log("-------------> 继续!")
    })

    /**
     * 可调节参数
     */
    const autoplayInput = folderParam.addInput(LEVEL, "autoplay", {
        label: "自动开始"
    })
    const speedInput = folderParam.addInput(LEVEL, "speed", {
        label: "运行速度",
        options: {
            "正常 500": 500,
            "放慢 1000": 1000,
            "极慢 2500": 2500,
            "加快 250": 250,
            "极快 100": 100
        }
    })

    autoplayInput.on("change", function(e) {
        console.log(`autoplay -> ${e.value}`)
        console.log(LEVEL.autoplay)
    })

    speedInput.on("change", function(e) {
        game.setSpeed(e.value)
        console.log(`-------------> 速度 ${e.value} !`)
    })

    /**
     * 状态数据
     */
    folderState.addMonitor(LEVEL.player.position, "x", {
        label: "玩家 x"
    })
    folderState.addMonitor(LEVEL.player.position, "y", {
        label: "玩家 y"
    })
    folderState.addMonitor(LEVEL, "speed", {
        label: "运行速度"
    })
    folderState.addMonitor(LEVEL, "money", {
        label: "金钱"
    })

</script>

<script>
	Vue.watchEffect(() => {
        pane.refresh()
    })

</script>
canvas#ok {
    background: white;
}

ul {
    list-style-type: none;
    padding-left: 0;
}

本项目引用的自定义外部资源