微信小程序 实现最简单的汉诺塔

原理

说原理,实际上没有什么原理,主要就是模拟汉诺塔拖动;主要困难点 应该是 微信小程序的页面布局、事件的响应等;

实现汉诺塔,主要需要实现的一个功能是 元素组件的拖拽,关于元素拖拽在这篇博客中已经有描述:https://www.cnblogs.com/q1076452761/p/15706227.html

接下来就是把各式各样的拖动后的逻辑 将其实现,然后体现在界面上;

代码

小程序首页

首页元素:

  1. 一张图片
  2. 标题
  3. 输入框
  4. 开始游戏 按钮

实现功能:

输入个数,然后点击按钮跳转到汉诺塔界面,生成相应数据量的木条数

xwml 代码

<view class="container">
  <image class="head-img" src="./yeah.jpg"></image>
  <view class="usermotto">
    <text class="user-motto">{{motto}}</text>
  </view>
  <form class="submit" catchsubmit="formSubmit">
    <input class="hanoi-count" type="number" placeholder="请输入数量(<=5)" name="input"></input>
    <button class="start-button" type="primary" form-type="submit">开始游戏</button>
  </form>
</view>

表单提交

  formSubmit(e) {
    console.log('form发生了submit事件,携带数据为:', e.detail.value); 
    var value = e.detail.value;
    wx.redirectTo({
      url: './game?count=' + value.input // 重定向至其他页面,并带上输入的个数
    })
  }

游戏界面

界面元素:

  1. 操作次数
  2. 三个区域
  3. 木条(stick)
  4. 返回按钮

实现功能:

  1. 木条初始化,摆放
  2. 木条拖拽
  3. 木条拖拽后校验是否可放入指定位置
  4. 操作次数累计
  5. 游戏完成校验

wxml

<!--index.wxml-->
<view>操作次数:{{operateCount}}</view>
<canvas class="gameCanavs" type="2d">
    <view class="items">

        <view class="vertical1">
            <view wx:for="{{stickListA}}" wx:key="index" style="text-align: center;">
                <view class="stick{{item.id}}" style="position: absolute; text-align: center;  width: {{item.width}}px; height:{{item.height}}px;background: rgb({{item.width-100}}, 180, {{item.width-100}});display: block;bottom: {{movingId == item.id ? movingBottomPos: item.bottomPos}}px; left: {{movingId == item.id ? strMovingPosX : leftPosA}}; transform:{{movingId == item.id ? untranslate : translate}};z-index:9999;" bindtouchmove="touchMove" bindtouchstart="touchStart" bindtouchend="touchEnd" id="item{{index}}" data-id="{{item.id}}" data-index="{{index}}">
                    <view>{{item.stickName}}</view>
                </view>
            </view>
        </view>

        <view class="vertical2">
            <view wx:for="{{stickListB}}" wx:key="index" style="text-align: center;">
                <view class="stick{{item.id}}" style="position: absolute; text-align: center;  width: {{item.width}}px; height:{{item.height}}px;background: rgb({{item.width-100}}, 180, {{item.width-100}});display: block;bottom: {{movingId == item.id ? movingBottomPos : item.bottomPos}}px; left: {{movingId == item.id ? strMovingPosX : leftPosB}}; transform:{{movingId == item.id ? untranslate : translate}};z-index:9999;" bindtouchmove="touchMove" bindtouchstart="touchStart" bindtouchend="touchEnd" id="item{{index}}" data-id="{{item.id}}" data-index="{{index}}">
                    <view>{{item.stickName}}</view>
                </view>
            </view>
        </view>

        <view class="vertical3">
            <view wx:for="{{stickListC}}" wx:key="index" style="text-align: center;">
                <view class="stick{{item.id}}" style="position: absolute; text-align: center;  width: {{item.width}}px; height:{{item.height}}px;background: rgb({{item.width-100}}, 180, {{item.width-100}});display: block;bottom: {{movingId == item.id ? movingBottomPos : item.bottomPos}}px; left: {{movingId == item.id ? strMovingPosX : leftPosC}}; transform:{{movingId == item.id ? untranslate : translate}};z-index:9999;" bindtouchmove="touchMove" bindtouchstart="touchStart" bindtouchend="touchEnd" id="item{{index}}" data-id="{{item.id}}" data-index="{{index}}">
                    <view>{{item.stickName}}</view>
                </view>
            </view>
        </view>

    </view>
</canvas>
<canvas class="horizontal" type="2d"></canvas>

<button type="primary" style="top: 100px;" bindtap='gotoMainPage'>返回</button>
  • 生成3个区域vertical1、vertical2、vertical3;对于摆放列表 stickListA、stickListB、stickListC
  • 元素解析
    • <view wx:for="{{stickListA}}" wx:key="index" style="text-align: center;"></view> 用于遍历木条列表
    • class="stick{{item.id}}" 用于点击木条时,可根据 "stickx" 标识来 找到当前木条的属性,如位置、长、宽等
    • position: absolute; 拖动时采用的时绝对位置,比较好计算
    • width: {{item.width}}px; height:{{item.height}}px; 木条长宽
    • background: rgb({{item.width-100}}, 180, {{item.width-100}});display: block; 木条样式颜色
    • bottom: {{movingId == item.id ? movingBottomPos : item.bottomPos}}px; left: {{movingId == item.id ? strMovingPosX : leftPosB}}; transform:{{movingId == item.id ? untranslate : translate}}; movingId 表示现在拖动的木条的id,和当前木条id对比;
      如果当前木条时拖动 则会位置会是
      • bottom = movingBottomPos;如10
      • left = strMovingPosX; 如"10px"
      • transform = untranslate; 作用用于木条居中设置,当拖动时 untranslate = "translate(0%, 0%)"
        当木条不是拖动的木条
      • bottom = item.bottomPos 木条原本位置
      • left = leftPosA、leftPosB、leftPosC 分别是16%、50%、84%对应三个区域
      • transform = translate;translate= "translate(-50%, 0%)" 配合left 用于水平居中
    • bindtouchmove="touchMove" bindtouchstart="touchStart" bindtouchend="touchEnd" 拖拽操作三兄弟,具体可以看上一篇博客:https://www.cnblogs.com/q1076452761/p/15706227.html
    • id="item{{index}}" data-id="{{item.id}}" data-index="{{index}}" 用于事件处理时能找到木条id数据

js 初始化数据

data: {
        operateCount: 0, // 操作次数
        count: 0, // 木条数
        maxStickLen: 85, // 木头最大长度 15差值递减
        stickHeight: 25,// 木头高度
        //拖动标记

        moving: false,
        movingId: -1,// 拖拽时的id 
        movingBottomPos: 0,// 拖拽过程中 变化 底部位置值
        movingOriginBottomPos: 0,// 当前拖拽的木条 的原始底部位置
        moveFromList: 0, // 当前拖拽的木条 来自于 哪个列表 1:A,2:B,3:C
        strMovingPosX: "0px",// 拖拽过程中 距离左边界的位置 字符串类型
        movingPosX: 0,// 拖拽过程中 距离左边界的位置 字符串类型

        moveStickOriginPosX: 0,// 当前拖拽木头的原始位置X
        moveStickOriginPosY: 0,// 当前拖拽木头的原始位置Y

        leftPosA: "16%",// 最终A 放置的位置 左侧的中间
        leftPosB: "50%",// 最终B 放置的位置 中间的中间
        leftPosC: "84%",// 最终C 放置的位置 右侧的中间
        translate: "translate(-50%, 0%)",// style 配合 leftPosA、leftPosB、leftPosC 居中
        untranslate: "translate(0%, 0%)",// style 拖拽时 设置为空

        touchStartX: 0,// 触摸点开始位置X
        touchStartY: 0,// 触摸点开始位置Y

        // 汉诺塔3列表
        stickListA: [],
        stickListB: [],
        stickListC: [],
        // 木头名称列表
        stickNameList: ["A", "B", "C", "D", "E", "F"],

        // 每个木条的所在列表
        stickIdinList: [],

        // 屏幕宽度,用于计算 拖动到 哪个部分
        screenWidth: 0, // 需要获取
    },

js 生成初始化木条

onLoad: function (options) {
        // 进入页面后,先获取屏幕的宽度(实际上是或class=".item"的view宽度)
        let query = wx.createSelectorQuery().in(this)
        query.select('.items').boundingClientRect()
        query.selectViewport().scrollOffset()
        var _this = this;
        query.exec(function (res) {
            _this.setData({
                screenWidth: res[0].width, // 得到屏幕宽度
            })
        })

        var tempStickList = [];
        var tempStickIdInList = [];
        // 构造木条
        for (var i = 0; i < options.count; i++) {
            var stick = { id: i, width: this.data.maxStickLen - i * 20, height: this.data.stickHeight, bottomPos: this.data.stickHeight * i, stickName: this.data.stickNameList[i] };// id、宽度20递减、高度25、bottomPos位置0->25->50;木条名称
            tempStickList.push(stick);
            tempStickIdInList.push(1);// 一开始 都在 1 : lsitA
        }
        this.setData({
            count: options.count, // 首页输入的木条数存储
            stickListA: tempStickList,// 将数据更新到内存
            stickIdinList: tempStickIdInList // 更新 每个木条的所在列表
        })
    },

js木条拖动

// 开始触摸
touchStart: function (e) {
        console.log("== touchStart ==");
        var id = e.currentTarget.dataset.id;// 得到当前的stickId
        // 需要校验是不是 头部
        var tempDargOriginBottomPos = 0;

        this.queryMultipleNodes(id);// 获取当前触摸的stick 位置信息

        var tempStickList = this.data.stickListA; // 拷贝临时数据进行处理
        if (this.data.stickIdinList[id] == 2) {
            tempStickList = this.data.stickListB;
        }
        else if (this.data.stickIdinList[id] == 3) {
            tempStickList = this.data.stickListC;
        }

        // 校验拖动的是不是顶部木条
        if (tempStickList[tempStickList.length - 1].id != id) {
            wx.showToast({
                title: '操作失败',
                icon: 'error',
                duration: 600
            })
            console.log("can not move, because it is not top stick");
            return
        }

        tempDargOriginBottomPos = tempStickList[tempStickList.length - 1].bottomPos; // 获取原始bottom坐标值
        
        // 准备好开始触摸的信息
        this.setData({
            moveFromList: this.data.stickIdinList[id], // 当前触摸的stick来自哪个List
            movingId: id,// 当前触摸的stick id
            touchStartX: e.touches[0].pageX,// 触摸点的开始位置
            touchStartY: e.touches[0].pageY,// 触摸点的开始位置
            moving: true, // 设置正在触摸
            movingOriginBottomPos: tempDargOriginBottomPos, // 记录当前触摸的stick 原始 bottom 值
            movingBottomPos: tempDargOriginBottomPos //触摸后 一开始的 bottom 值
        });
    },

// 触摸移动

touchMove: function (e) {
        if (!this.data.moving) return;
        var bottomPoxCha = e.touches[0].pageY - this.data.touchStartY;// 计算得到 当前触摸点 和 开始触摸点 的纵轴差值
        var PosXCha = e.touches[0].pageX - this.data.touchStartX;// 计算得到 当前触摸点 和 开始触摸点 的横轴差值
        var PosX = this.data.moveStickOriginPosX + PosXCha; // 得到 移动后的 横向坐标值

        this.setData({
            movingBottomPos: this.data.movingOriginBottomPos - bottomPoxCha, // 记录 移动后的 纵向坐标值
            strMovingPosX: PosX + "px",// 记录 移动后的 横向坐标值 字符串 todo
            movingPosX: PosX// 记录 移动后的 横向坐标值
        });
    },

// 触摸结束
touchEnd: function (e) {
        if (!this.data.moving) return;
        console.log("== touchEnd ==")
        // 拖动结束 需要判断是否 可以放入到当前位置的List中
        var tempOldStickList = this.data.stickListA;
        if (this.data.moveFromList == 2) {
            tempOldStickList = this.data.stickListB;
        }
        else if (this.data.moveFromList == 3) {
            tempOldStickList = this.data.stickListC;
        }

        var stickLen = tempOldStickList[tempOldStickList.length - 1].width;
        var stickMidPosX = this.data.movingPosX + stickLen / 2.0; // 得到中心位置

        var _this = this;
        var inList = this.data.moveFromList;

        // 可以放入的区域
        if (stickMidPosX < this.data.screenWidth / 3.0) // 放置在左侧
        {
            var tempNewStickList = this.data.stickListA;
            this.moveStickList(inList, 1, tempOldStickList, tempNewStickList)
        }
        else if (stickMidPosX < this.data.screenWidth * 2.0 / 3.0 && this.data.screenWidth / 3.0 <= stickMidPosX) // 放置在中间
        {
            var tempNewStickList = this.data.stickListB;
            this.moveStickList(inList, 2, tempOldStickList, tempNewStickList)
        }
        else // 放置在 右侧
        {
            var tempNewStickList = this.data.stickListC;
            this.moveStickList(inList, 3, tempOldStickList, tempNewStickList)
        }
        
        // 数据恢复
        this.setData({
            moving: false,
            movingId: -1,
            touchStartX: 0,
            touchStartY: 0,
            moveFromList: 0,
            movingOriginBottomPos: 0,
            movingBottomPos: 0,
        });
        // 检查是否完成
        this.checkFinish()
    },

其余函数

// 根据木条id查询当前(触摸的)木条的信息,并更新
queryMultipleNodes: function (id) {
        let query = wx.createSelectorQuery().in(this)
        var key = ".stick" + id; // 根据 class=".stickX" 来捕获stick信息
        query.select(key).boundingClientRect()
        query.selectViewport().scrollOffset()
        var _this = this;
        query.exec(function (res) {
            var tempLeftPos = res[0].left;
            _this.setData({
                moveStickOriginPosX: res[0].left, // 记录stick的原始坐标值
                moveStickOriginPosY: res[0].top,// 记录stick的原始坐标值
                movingPosX: res[0].left,
                strMovingPosX: tempLeftPos + "px"
            })
        })
    },

// 木条的移动,将stick从oldList取出,放入newStickList,并计算得到新的bottomPos

moveStickList: function (oldList, newList, oldStickList, newStickList) {
        console.log("moveStickList: move " + oldList + " to " + newList);
        if (oldList == newList) return;
        var oldStickLen = oldStickList.length;
        var newStickListLen = newStickList.length;
        var tempStick = oldStickList[oldStickLen - 1];

        if (newStickListLen < 1) {
            tempStick.bottomPos = this.data.stickHeight * newStickListLen;
            newStickList.push(tempStick);
            oldStickList.splice(oldStickLen - 1, 1);
        }
        else {
            if (newStickList[newStickListLen - 1].id < tempStick.id) {
                tempStick.bottomPos = this.data.stickHeight * newStickListLen;
                newStickList.push(tempStick);
                oldStickList.splice(oldStickLen - 1, 1);
            }
            else {
                console.log("it is bigger than top stick, can not move");
                return
            }
        }

        var tempStickIdInList = this.data.stickIdinList;
        tempStickIdInList[this.data.movingId] = newList;
        if (oldList == 1) {
            this.setData({
                stickListA: oldStickList
            })
        } else if (oldList == 2) {
            this.setData({
                stickListB: oldStickList
            })
        }
        else if (oldList == 3) {
            this.setData({
                stickListC: oldStickList
            })
        }

        if (newList == 1) {
            this.setData({
                stickListA: newStickList,
                stickIdinList: tempStickIdInList
            })
        } else if (newList == 2) {
            this.setData({
                stickListB: newStickList,
                stickIdinList: tempStickIdInList
            })
        }
        else if (newList == 3) {
            this.setData({
                stickListC: newStickList,
                stickIdinList: tempStickIdInList
            })
        }
        var opCount = this.data.operateCount + 1;
        this.setData({
            operateCount: opCount
        })
    },

// 检查是否完成
checkFinish: function () {
        if (this.data.stickListC.length == this.data.count) {
            var strResult = "操作次数为:" + this.data.operateCount;
            wx.showModal({
                title: '恭喜过关',
                content: strResult,
                success(res) {
                    if (res.confirm) {
                        console.log('用户点击确定')
                        wx.navigateTo({ url: './index', })
                    } else if (res.cancel) {
                        console.log('用户点击取消')
                    }
                }
            })
        }
    },

实现效果

首页

游戏界面

游戏完成

源码

github: https://github.com/KOGLinese/WechatHanoi

其他遇到的坑

  • 微信小程序申请的appId后,不能把自己的小程序类别设置为游戏;不然会编译错误,提示找不到 game.json;感觉这是微信小程序开发的大bug,只要appid是小游戏类型,就必须安装小游戏的模板来;不能进行空白小程序的开发;
posted @ 2022-01-19 20:42  Linese  阅读(198)  评论(0编辑  收藏  举报