微信小程序 实现最简单的汉诺塔
原理
说原理,实际上没有什么原理,主要就是模拟汉诺塔拖动;主要困难点 应该是 微信小程序的页面布局、事件的响应等;
实现汉诺塔,主要需要实现的一个功能是 元素组件的拖拽,关于元素拖拽在这篇博客中已经有描述:https://www.cnblogs.com/q1076452761/p/15706227.html
接下来就是把各式各样的拖动后的逻辑 将其实现,然后体现在界面上;
代码
小程序首页
首页元素:
- 一张图片
- 标题
- 输入框
- 开始游戏 按钮
实现功能:
输入个数,然后点击按钮跳转到汉诺塔界面,生成相应数据量的木条数
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 // 重定向至其他页面,并带上输入的个数
})
}
游戏界面
界面元素:
- 操作次数
- 三个区域
- 木条(stick)
- 返回按钮
实现功能:
- 木条初始化,摆放
- 木条拖拽
- 木条拖拽后校验是否可放入指定位置
- 操作次数累计
- 游戏完成校验
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.htmlid="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是小游戏类型,就必须安装小游戏的模板来;不能进行空白小程序的开发;