H5 游戏开发启蒙案例 05 《数字华容道》

个人开发者。 策划、程序、美术全垒打。 公众号《怪诞编程》。

文章正文

如何用 197 行代码,实现一个《数字华容道》游戏案例,面向的平台依旧是 H5,语言为 JavaScript

玩法介绍

把一堆打乱的数字,最经典的为 3*3 的大小,通过点击移动,还原回按顺序的排列,其中总有一个格子是空的,用来移动,这里的设定,最后一个格子总是为空的

3*3

5*5 的就相对比较难的,可以根据自己需要去配置

5*5

思路介绍

格子:

{
    //原始的坐标,判断游戏胜利的时候需要 posOrg 要等于 pos
    posOrg = {x:0,y:0} ,
    //现在的坐标 格子是可以交换位置的
    pos = {x:0,y:0} ,
    //是否是空白的 当点击一个格子,寻找它周围是否有 isEmpty == true 的格子,如果有,进行交换位置
    isEmpty = false
    //当前格子的数字,显示用
    number = 1
}

v2:

//封装的一个方法,传入 x,y 坐标,返回一个对象
let v2 = (x, y) => {
    return {
        x: x,
        y: y
    }
}

地图:

//用一个一位数组,因为格子的位置不固定
m.map = [格子,格子,...]

事件:

点击事件,可以获取到坐标轴,把这个坐标再换算成地图的坐标,可以找到对应的格子
然后判断周围是否有空格,有的话交换位置,然后步数加 1,判断游戏胜利条件等

配置:

地图的宽、高、格子大小,颜色这些都开出配置
可以在模型层直接修改,即生效

这是几个关键的点,还设涉及到一些生成地图的算法,打乱数据的算法,交换位置的算法。在后续文章中说明。

MVC 模板

这个模板已经用到了现在的第 N 个案例了,为什么还要写出来,就因为这个简单好用,如果你也有新游戏的想法,而无从下手时,不妨也拿这个来开始

<html>

<body>
    <canvas id="canvas" style="position: absolute;top: 50%;left: 50%;border: 1px solid #4a4a4a;"></canvas>
</body>

<script type="text/javascript" language="JavaScript" charset="UTF-8">
    //======== 模型
    let m = {}
    //======== 视图
    let v = {}
    //======== 控制
    let c = {
        main () { },
    }
    c.main()
</script>

</html>

实现过程

  • M 模型层: 负责存储数据、数据逻辑
let m = {
    //===常量===
    //地图宽度
    MapWidth: 3,
    //地图高度
    MapHeight: 3,
    //单位格子大小(像素)
    UnitSize: 50,
    //颜色
    Color: {
        grey: "#4a4a4a",
        red: "#ffff00",
        white: "#ffffff",
    },
    //===变量===
    //地图
    map: [
        //原始的坐标 posOrg
        //现在的坐标 pos
        //是否是空白的 isEmpty
        //当前格子的数字,显示用 number
    ],
    //游戏状态 0 游戏中 1 过关
    status: 0,
    //步数
    step: 0,
    //鼠标移动到哪个坐标。用来绘制选中的格子光标
    mouseMoveVec: null,
    //===逻辑===
    //初始化模型层
    init() {
    },
    //鼠标移动到哪里
    onMouseMove(offsetX, offsetY) {
        //转成坐标
            //在地图内,记录起来
    },
    //点击了格子
    onClick(offsetX, offsetY) {
        //转成坐标
        //找出点击的格子
        //循环寻找周围是否有空格
            //判断这个空格是否在点击的格子的周围
            //符合移动条件,交换位置
    },
    //新的数据
    newData() {
        //生成地图
        //打乱数据
    },
}
  • V 视图层: 将数据可视化表达
let v = {
    //初始化
    init() {

    },
    //执行绘制
    onDraw() {
        //绘制地图
            //绘制格子
        //绘制鼠标选中的格子
        //绘制步数等显示状态
    },
}
  • C 控制层: 负责从控制用户输入,并向模型发送数据,执行主循环
let c = {
    //入口函数
    main() {
    },
    //初始化输入监听
    initInputListener() {
        //点击
        //鼠标移动
        //鼠标移出
    },
}

以上为整体流程的思路,按照 MVC 来设计,再设计好数据结构,把对象和行为这些都列好,接下来就非常容易实现了。文末附上所有完整源码。

完整源码

如果文章不明白,没关系,右键新建一个文本文档 .txt,改名为 index.html。然后把以下代码复制进去,保存,再双击打开。

就是这个案例的全部源码了,代码量不多,笔者也写的不好,讲解的不到位,并不好,请谅解,谢谢。

请收下这份 256 行的源码:

  • 扣掉注释 49 行
  • 扣掉空行 10 行
  • 实际的代码行数 197 行

index.html

<html>
<title>数字华容道</title>

<body>
    <canvas id="canvas" style="position: absolute;top: 50%;left: 50%;border: 1px solid #4a4a4a;"></canvas>
</body>
<script type="text/javascript" language="JavaScript" charset="UTF-8">
    //======== 模型
    //封装的一个方法,传入 x,y 坐标,返回一个对象
    let v2 = (x, y) => {
        return {
            x: x,
            y: y
        }
    }
    let m = {
        //===常量===
        //地图宽度
        MapWidth: 3,
        //地图高度
        MapHeight: 3,
        //单位格子大小(像素)
        UnitSize: 50,
        //颜色
        Color: {
            grey: "#4a4a4a",
            red: "#ffff00",
            white: "#ffffff",
        },
        //===变量===
        //地图
        map: [
            //原始的坐标 posOrg
            //现在的坐标 pos
            //是否是空白的 isEmpty
            //当前格子的数字,显示用 number
        ],
        //游戏状态 0 游戏中 1 过关
        status: 0,
        //步数
        step: 0,
        //鼠标移动到哪个坐标。用来绘制选中的格子光标
        mouseMoveVec: null,
        //===逻辑===
        //初始化模型层
        init() {
            m.status = 0
            m.step = 0
            m.map = this.newData()
        },
        //鼠标移动到哪里
        onMouseMove(offsetX, offsetY) {
            if (m.status == 1) {
                return
            }
            //转成坐标
            let x = Math.floor(offsetX / m.UnitSize)
            let y = Math.floor(offsetY / m.UnitSize)
            if (x >= 0 && x < m.MapWidth && y >= 0 && y < m.MapHeight) {
                //在地图内,记录起来
                m.mouseMoveVec = {
                    x: x,
                    y: y
                }
            } else {
                m.mouseMoveVec = null
            }
        },
        //点击了格子
        onClick(offsetX, offsetY) {
            if (m.status == 1) {
                m.init()
                return
            }
            //转成坐标
            let x = Math.floor(offsetX / m.UnitSize)
            let y = Math.floor(offsetY / m.UnitSize)

            let arr = m.map
            //点击的格子
            let clickBox
            for (let i = 0; i < m.map.length; i++) {
                if (m.map[i].pos.x == x && m.map[i].pos.y == y) {
                    //找出点击的格子
                    clickBox = m.map[i]
                }
            }
            //循环寻找周围是否有空格
            for (let i = 0; i < arr.length; i++) {
                let B = arr[i]
                if (!B["isEmpty"]) {
                    continue
                }
                //判断这个空格是否在点击的格子的周围
                if (clickBox.pos.x - 1 == B.pos.x && clickBox.pos.y == B.pos.y) {

                } else if (clickBox.pos.x + 1 == B.pos.x && clickBox.pos.y == B.pos.y) {

                } else if (clickBox.pos.x == B.pos.x && clickBox.pos.y - 1 == B.pos.y) {

                } else if (clickBox.pos.x == B.pos.x && clickBox.pos.y + 1 == B.pos.y) {

                } else {
                    //不符合交换条件
                    continue
                }
                //符合移动条件,交换位置
                let posA = clickBox.pos
                clickBox.pos = B.pos
                B.pos = posA
                m.step++
                return
            }

        },
        //新的数据
        newData() {
            let number = 1
            let arr = []
            //生成地图
            for (let y = 0; y < m.MapHeight; y++) {
                for (let x = 0; x < m.MapWidth; x++) {
                    let bean = {
                        number: number,
                        posOrg: v2(x, y),
                        pos: v2(x, y),
                    }
                    arr.push(bean)
                    number++
                }
            }
            arr[arr.length - 1].isEmpty = true
            //打乱数据
            for (let i = 0; i < arr.length; i++) {
                let A = arr[i]
                let B = arr[Math.floor(Math.random() * arr.length)]
                let posTemp = A.pos
                A.pos = B.pos
                B.pos = posTemp
            }
            return arr
        },
    }
    //======== 视图
    let v = {
        //初始化
        init() {
            let canvas = document.getElementById("canvas")
            v.canvas = canvas
            //设置 canvas 的宽高
            canvas.width = m.MapWidth * m.UnitSize
            canvas.height = m.MapHeight * m.UnitSize + m.UnitSize * 1
            //宽高变化重新调整 canvas 在页面中的位置,使居中
            canvas.style.marginLeft = -canvas.width / 2
            canvas.style.marginTop = -canvas.height / 2
            //拿到 canvas 绘制的上下文,方便之后直接进行绘制
            this.context = canvas.getContext("2d")
        },
        //执行绘制
        onDraw() {
            let context = this.context
            let canvas = v.canvas
            context.clearRect(0, 0, canvas.width, canvas.height)
            context.lineWidth = 1
            //绘制地图
            let dx = 1
            let dy = 1
            for (let i = 0; i < m.map.length; i++) {
                let bean = m.map[i]
                if (bean.isEmpty) {
                    continue
                }
                //绘制格子
                let x = bean.pos.x
                let y = bean.pos.y

                context.fillStyle = m.Color.grey
                context.beginPath()
                context.rect(
                    x * m.UnitSize + dx,
                    y * m.UnitSize + dy,
                    m.UnitSize - dx * 2,
                    m.UnitSize - dy * 2)
                context.fill()
                context.fillStyle = m.Color.white
                context.textAlign = "center"
                context.textBaseline = "middle"
                context.font = m.UnitSize * 0.8 + "px Fantasy"
                context.fillText(bean.number + "", (x + 0.5) * m.UnitSize, (y + 0.5) * m.UnitSize)
            }
            //绘制鼠标选中的格子
            if (m.mouseMoveVec) {
                context.strokeStyle = m.Color.red
                context.lineWidth = 2
                context.beginPath()
                context.rect(
                    m.mouseMoveVec.x * m.UnitSize + dx,
                    m.mouseMoveVec.y * m.UnitSize + dy,
                    m.UnitSize - dx * 2,
                    m.UnitSize - dy * 2)
                context.stroke()
            }
            //绘制步数等显示状态
            context.fillStyle = m.Color.grey
            context.font = m.UnitSize * 0.5 + "px Fantasy"
            context.textAlign = "center"
            if (m.status == 1) {
                context.fillText("过关" + m.step, canvas.width / 2, canvas.height - m.UnitSize / 2)
            } else {
                context.fillText("步数 " + m.step, canvas.width / 2, canvas.height - m.UnitSize / 2)
            }
        },
    }
    //======== 控制
    let c = {
        //入口函数
        main() {
            c.initInputListener()
            v.init()
            m.init()
            v.onDraw()
        },
        //初始化输入监听
        initInputListener() {
            //点击
            document.getElementById("canvas").addEventListener('click', function (event) {
                m.onClick(event.offsetX, event.offsetY)
                v.onDraw()
            }, false);
            //鼠标移动
            document.getElementById("canvas").addEventListener('mousemove', function (event) {
                m.onMouseMove(event.offsetX, event.offsetY)
                v.onDraw()
            }, false);
            //鼠标移出
            document.getElementById("canvas").addEventListener('mouseout', function (event) {
                m.onMouseMove(event.offsetX, event.offsetY)
                v.onDraw()
            }, false);

        },
    }
    //=== 启动
    c.main()
    let 不想努力了 = function () {
        console.log("执行自动过关秘籍。\n 更多源码案例 小刀 UP qq378741819 \n 微信公众号《怪诞编程》")
        m.status = 1
        for (let i = 0; i < m.map.length; i++) {
            m.map[i].pos = m.map[i].posOrg
            m.map[i].isEmpty = false
        }
        v.onDraw()
    }
</script>

</html>

本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

作者正在撰写中...
内容互动
写评论
加载更多
评论文章
× 订阅 Java 精选频道
¥ 元/月
订阅即可免费阅读所有精选内容