软工导论-2-记忆广度(微信小程序)的设计开发实装
目录
记忆广度-微信小程序开发-个人总结
博客班级 | https://edu.cnblogs.com/campus/zjcsxy/SE2020 |
---|---|
作业要求 | https://edu.cnblogs.com/campus/zjcsxy/SE2020/homework/11633 |
班级 | 计算机1803 |
姓名 | 章一砚 |
学号 | 31801146 |
记忆广度是什么:游戏设计与规则介绍
- 规则:给出一个用方块拼接成的图形,每个方块中有一个数字,图形显示的时间根据难度设定,三个难度等级设定的时间分别为5秒,10秒,15秒,图形显示结束后打乱方块顺序,要求被测者将方块拖回原来的位置。
结束条件:连续做错3题或完成全部的题目;
每题时间限制:90秒;
评分:做对1题得1分,共18题,最高分为18分
难度一:由五个数字方块组成的图形
难度二:由六个数字方块组成的图形
难度三:由七个数字方块组成的图形 - GIF演示
游戏设计 - 界面:
- 棋盘:用于摆放方格。我们设计一个 4 × 4 的棋盘,用于摆放方格。(之后,我们会使用“棋子”这个称呼来代替方格)
- 方格(棋子):方格应该有三种状态【空白格子、棋盘底色格子、有数字的格子】
- 方格(棋子)拼接成的图形:应该是在这个 4 × 4 的棋盘中随机生成的一个图形
- 方格(棋子)待选区:将之前在棋盘中出现过的数字方格打乱顺序,放置在棋盘下方的待选区。
- 状态栏:包括
- 当前游戏状态【等待开始、请记忆棋盘、请拖动棋子(等待提交答案)】
- 当前游戏进度【第 1 / 6 关】
- 当前环节倒计时【还剩 ... 秒】
- 按钮组:至少有一个 “提交答案” 的按钮,用于在等待用户拖动棋子的倒计时中,让用户主动提交答案,进入下一个环节。
游戏设计 - 数据:
- 棋盘大小
board_size
:设定为 4 × 4 即可,没有变化的必要 - 棋子个数
level
:有数字的、需要记忆的棋子个数 —— 暂定为 5 6 7 个,需要支持使用方便的参数进行动态的修改。 - 记忆耗时
level_time
:对于不同的棋子个数,记忆的时间暂定为 —— 5 10 15 秒,需要支持使用方便的参数进行动态的修改。 - 棋子拖动耗时常量即可:最多不超过10个棋子需要被拖动。为保险起见,设定30秒用于等待拖动,绰绰有余。
- 棋盘中的每个棋子是什么:
- 使用一个
Number[] board_num[16]
来保存。 - 数组元素为 -1 的,应该是空白棋子,不然,就是数字棋子。
- 使用一个
- 棋子本身的相关信息:
- 棋子是哪个数字对应的棋子——数字本身
- 棋子在本次游戏中,被放在了棋盘的哪个位置
- 可以从
board_num[16]
中遍历查得。 - 但是为了方便,不如再设计一个数组
chess_index[level]
第 i 个
棋子,在本局中的初始位置是chess_index[i]
- 可以从
- 棋子现在被放在了哪个位置
chess_nowAt[16]
-1
表示棋子正处于待选区- 否则,表示棋子正处于棋盘的某个位置
- 如果
chess_index[i] = chess_nowAt[chess_index[i]]
则棋子位置符合答案要求
- 棋子动态拖动需要的相关信息:
chess_start[16]
用于保存棋子初始位置。chess_move[16]
用于保存棋子位移。- 棋子初始位置:
- 这个初始位置指的是棋子在待选区时的初始位置。
- 棋子位移:
- 这个位移指的是棋子当前位置和在待选区的位置的相对位移。
- 棋子当前位置:
- 棋子初始位置 + 棋子位移 = 棋子当前位置
pos_table[16]
用于保存棋盘中的16个格子各自的坐标位置。- 当需要获取棋子当前位置是否是棋盘中的某个格子时
- 遍历
pos_table[16]
,与棋子当前位置进行比较 - 误差允许则返回匹配到的位置在
pos_table[16]
中的索引值 - 否则,返回
-1
chess_zindex[16]
用于保存棋子的CSS属性z-index
的值- 我们希望棋子在被拖动时,应该是 z-index 值最大的,不会被别的UI元素覆盖遮挡住
各功能 具体实现 复盘
生成一个随机的棋盘
初级需求
- 这个棋盘应该是一个长为 16 的数字数组
- 这个数组中 有
level
个不是 -1 的数- 不是 -1 的数,包括 1,2,3,4,5,6,7,8,9
- 注意,不含 0
- 棋盘应该是随机的
初级需求的实现
原地随机一个数组
// 有很多种写法,反正,写起来不难
/**
* 打乱一个数组(原地打乱)
* @param {Number[]} arr 需要被打乱的数组
* @returns {Number[]} 被打乱后的数组的引用(其实和被传入的引用是同一个)
*/
function randArr(arr) {
for (var i = 0; i < arr.length; i++) {
var iRand = parseInt(arr.length * Math.random());
[arr[i], arr[iRand]] = [arr[iRand], arr[i]];
}
return arr;
}
获取初级需求的一个随机棋盘
/**
* 建立随机的初始棋盘
* @param {Number} n 有 n 个有效的格子
* @param {Number} size 总共有 size 个格子
* @returns {Number[]} 包含`n`个不同的正整数和`size-n`个`-1`的一维数组
*/
function get_Random_board(n, size) {
let base = [1, 2, 3, 4, 5, 6, 7, 8, 9];
randArr(base);
let out = base.slice(0, n);
for (let i = n; i < size; i++) {
out.push(-1);
}
randArr(out);
// prettyBoard(out); /* 这一步是进阶需求:整理优化棋盘 */
return out;
}
进阶需求
- 按初级需求中的方式生成的棋盘,是随机的。
- 这样的随机同时会导致棋子在棋盘上的散乱分布。
- 作为记忆广度的棋盘,我们不希望棋子过于散乱分布。
- 我们希望棋子尽量是一定程度上连续的。
进阶需求的实现
检查棋子图形是否足够连续
- 检查棋子连续性,其实也就是,检查棋子是不是”连通图“
- 用 BFS/DFS 遍历之即可
/**
* 检查棋盘数组是否足够聚拢
* @param {Number[]} board 被检查的棋盘数组
* @returns {Boolean} 返回`True` or `False`
*/
function isOK(board) {
let start = -1;
let totalCnt = 0;
let stack = []; /* 预备查看的索引值 栈 */
let isVisited = []; /* -1:无需访问 0:已经访问 >0:需要访问还未访问 */
let step_cnt = 0;
/* 遍历出所有合法点个数 */
for (let i = 0; i < board.length; i++) {
if (board[i] != -1) {
totalCnt++;
/* 保存第一个合法点的下标 */
if (start == -1) {
start = i;
}
}
isVisited.push(board[i]);
}
stack.push(start);
do {
let x = stack.pop(); /* 取出并保存本次局部搜索的起点索引 */
let tars = [
x - 1,
x + 1,
x - 4,
x + 4,
]; /* 本次局部搜索四个方向上的索引 */
tars.forEach((tar) => {
/* 如果坐标未越界 */
if (tar >= 0 || tar < board.length) {
/* 当前节点待遍历 */
if (isVisited[tar] > 0) {
isVisited[tar] = 0; /* 标记为已经遍历 */
step_cnt++; /* 有效值计数器自增 */
stack.push(tar); /* 当前位置标记为下一个起点 */
} /* 否则(≤0),无需访问 */
}
});
} while (stack.length > 0);
return step_cnt == totalCnt;
}
调整棋盘使图形聚拢
- 将整个 4 × 4 理解为 中间的一个 2 × 2 和外面的一圈
- 遍历外面的一圈棋子,尝试让他们向内移动一格
- 如果外圈棋子内的格子是有棋子的,则不移动
- 否则,向内移动一格
/**
* 调整`i`号位的棋子
* @param {Number[]} 待调整的棋盘数组
* @param {Number} i 这一步想调整的棋子的索引值
*/
function step(board, i) {
/* 如果 位置上没有数字 or i是在中间的那四个格子(索引分别是 5 6 9 10) */
if (board[i] == -1 || [5, 6, 9, 10].indexOf(i) > -1) {
/* 则什么都不做 */
return;
}
/* 如果 是第1列 */
if (i % 4 == 0) {
/* 则 向右 挪动一格 */
if (board[i + 1] == -1) {
[board[i], board[i + 1]] = [board[i + 1], board[i]];
}
} else if (i % 4 == 3) {
/* 如果 是第4列 */
/* 则 向左 挪动一格 */
if (board[i - 1] == -1) {
[board[i], board[i - 1]] = [board[i - 1], board[i]];
}
} else if (i / 4 < 1) {
/* 如果 是第1行 */
/* 则 向下 挪动一格 */
if (board[i + 4] == -1) {
[board[i], board[i + 4]] = [board[i + 4], board[i]];
}
} else if (i / 4 >= 3) {
/* 如果 是第4行 */
/* 则 向上 挪动一格 */
if (board[i - 4] == -1) {
[board[i], board[i - 4]] = [board[i - 4], board[i]];
}
}
}
组合棋盘聚拢和连通性检查
- 遍历并尝试移动一遍以后,再次检查棋盘是否足够连通
- 如果还没有,那就再遍历一轮并尝试移动
- 如此,往复几轮,基本上能让棋盘成为连通图
- 既使还没完全连通,也问题不大了(不是过于散乱了)
/**
* 整理棋盘,使棋子聚拢向中间
* @param {Number[]} board 需要被聚拢的棋盘数组(原地聚拢)
*/
function prettyBoard(board) {
for (let k = 0; k < 3; k++) {
if (!isOK(board)) { for (let i = 0; i < board.length; i++) { step(board, i); } }
else { break; }
}
}
棋盘大小及棋子大小的动态设定
- 这个棋盘的棋子个数是固定 4 × 4 的
- 因此,棋子大小只与屏幕大小有关
- 棋盘是方形的,长度和宽度相同
- 按照团队协作规范,棋盘宽度设定为屏幕宽度的80%
/* 获取设备屏幕大小 */
let that = this;
wx.getSystemInfo({
success(res) {
that.windowWidth = res.windowWidth; /* 屏幕宽度 */
that.windowHeight = res.windowHeight; /* 屏幕高度 */
that.chess_size = (res.windowWidth * 0.8) / 7, /* 待选区棋子大小 */
},
});
/* WXML 棋盘(中每个棋子的大小) */
<view style="width:calc({{(windowWidth*0.8)/4}}px);height:calc({{(windowWidth*0.8)/4}}px );" />
/* WXML 棋子(待拖拽) */
<view class="chess_drag" style="width:{{chess_size}}px;height:{{chess_size}}px" />
棋子可拖拽的实现
-
棋子的定位由 left 和 top 实现
-
若 left. top 的是0,则意味着棋子在一开始的位置,没动。
-
棋子样式(WXML)属性值
style="z-index:{{chess_zindex[item]}};left:{{chess_move[item].left}}px;top:{{chess_move[item].top}}px;"
-
WX 的拖拽事件,可以在WXML中设置属性值传递进
event
的dataset
<view
class="chess_drag"
wx:for="{{chess_index}}"
wx:key="item"
id="chess_{{board_num[item]}}"
data-who="{{item}}"
data-i="{{index}}"
catchtouchstart="moveStart"
catchtouchmove="handleMove"
catchtouchend="moveEnd"
style="z-index:{{chess_zindex[item]}};left:{{chess_move[item].left}}px;top:{{chess_move[item].top}}px;">
<image src='{{board_img_url[item]}}' style="width:{{chess_size}}px;height:{{chess_size}}px" />
</view>
棋子的整个拖拽过程分为三部分
触摸开始
catchtouchstart="moveStart"
- 如果发现棋子是第一次被触摸(
chess_start[who]
的 left 和 top 都是 0) - 那么获取当前触摸事件的被点击对象的位置,存储进
chess_start[who]
- 同时,为了让被拖动的棋子元素是是浮动在上方,所以设一个大一点的 z-index 值
/**
* 处理 触摸开始
* @todo 初始化棋子的初始位置
* @todo 被拖动的棋子 z-index 调高至200
*/
moveStart: function (event) {
if (this.data.game_state != "开始拖动吧") { return; }
let who = event.currentTarget.dataset.who;
let chess_start = this.data.chess_start;
let param = {};
if (chess_start[who].left == 0 && chess_start[who].top == 0) {
param["chess_start[" + who + "]"] = {
/**
* 此处,根据棋子大小做了矫正。
* 因为 WXML 的元素定位点并不是元素中心,而是(大概)左上角的一个位置。
* 设置了 display: inline 所以定位点是左边中间靠下一点点的一个位置。
* 进行了宽度的一半,高度的四分之一的位置修正后,定位点就是元素中心了。
*/
left: event.currentTarget.offsetLeft + this.data.chess_size / 2,
top: event.currentTarget.offsetTop - this.data.chess_size / 6,
};
}
param["chess_zindex[" + who + "]"] = 200;
this.setData(param);
}
触摸移动中
catchtouchmove="handleMove"
- 通过
event.currentTarget.dataset.who
得知现在是哪个棋子被拖动了 - 通过
this.data.chess_start[who]
得知这个棋子一开始的初始坐标,便于后面计算位置偏移量 - 通过
event.changedTouches[0].pageX
得知本次触摸点的 X 坐标。Y 坐标同理。 - 检查这个坐标是否符合棋盘某个格子的要求
- 若符合要求,则,设置位移为格子坐标减去棋子初始坐标
- 不符合要求,则,设置位移为当前触摸坐标减去棋子初始坐标
- 位移的更新到
chess_move[who]
,实际效果就体现为棋子被手指拖着到哪就到哪
触摸结束
catchtouchend="moveEnd"
- 和之前同理,从
event
中取数据,检查坐标是否符合棋盘某个格子的要求- 若符合要求,则,啥也不做
- 不符合要求,则,设置位移为
{left:0, top:0}
使棋子回到待选区
- 拖动已经结束了,z-index 可以调回来,从200改回100
棋盘各个格子的初始位置的读取
- 用微信提供的查询API,获取每个格子节点
- 遍历得到的节点序列,并获取其位置,然后存储
- 最重要的是:
- 因为棋盘有一个入场加载动画是微信自带的
- 为了保证获取到的棋盘是真的棋盘
- 而不是动画中飞到半路的棋盘
- 我们设置一个两秒的延时
- 延时以后再进行查询
/* 延迟两秒后再更新棋盘位置表,避免出现错误 */
setTimeout(() => {
let query = wx.createSelectorQuery();
query.selectAll('.chess_map > .chess_box').boundingClientRect();
query.exec((res) => {
let pos_table = [];
res[0].forEach((e) => {
pos_table[parseInt(e.id)] = {
// left: e.left,
// right: e.right,
// top: e.top,
// bottom: e.bottom,
// centerX: (e.left + e.right) / 2,
// centerY: (e.top + e.bottom) / 2,
left: (e.left + e.right) / 2,
top: (e.top + e.bottom) / 2,
}
});
// console.log(pos_table);
that.setData({
pos_table: pos_table
});
});
}, 2000);
最后总结
最后实装的主要内容就在这里了。其实中间还有一些失败的技术探索,各种半成品。真正花了大量时间的就是这些爬坑的过程。