1. 游戏截图
本图在网络中寻找的,突发奇想怀旧一下
2. 思路
- 看到游戏面板是一个正方形,每列每行的方块都是相同的
- 有一个空白区域可供玩家进行换位,最终拼成图案
- 在空格上下左右方向的可以换位
3. 大框架
大框架的宽高是由每列每行的小方块决定的,假设小方块宽等于50那高也等于50,每行4个小方块,那大框架的宽高就等于4*50
4. 代码
1. 创建一个Game类并创建初始化变量
class Game {
constructor() {
this.boxSize = 100; // 小格子的大小
this.cvs = document.getElementById("canvas");
this.ctx = this.cvs.getContext("2d");
// 边距
this.margin = {left:0,top:0}; // canvas元素的外边距
this.level = 0; // 关卡难度
this.levels = [3,4,5]; // 每关对应的方块数量
this.levelArr = []; // 呈现在画布上的数组
this.levelCopy = []; // 成功的数据
}
}
2. 编写init方法
在这里我们插入了一个"zzz"代表空白处,levelArr就是levelCopy打乱后的结果
initLevel() {
this.cvs.width = this.cvs.height = this.boxSize * this.levels[this.level];
this.getMargin();
this.levelCopy = [];
this.levelArr = [];
const levelNum = this.levels[this.level];
const count = levelNum * levelNum - 1;
this.levelCopy = Array.from({length: count}, (_, index) => index + 1);
this.levelCopy.push('zzz');
this.levelArr = this.shuffleArray([...this.levelCopy]);
this.draw();
}
// 打乱数组
shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
3. 绘制levelArr
这里使用一维数组进行绘制,但是需要绘制的是正方形的结果,可以想到每列每行的数量都是根据“this.levels[this.level]”来决定的,假设val值为3,每次循环到"i % val === 0"就代表循环的一行,就需要换行渲染。
我使用pointer对象来记录每个方块渲染的位置。
结果图:
到这里绘制就基本差不多了
draw() {
this.ctx.clearRect(0,0,this.cvs.width,this.cvs.height);
this.drawRectLine(0,0,this.cvs.width,this.cvs.height);
const levelNum = this.levels[this.level];
const pointer = {x:0,y:0};
for(let i = 1; i <= this.levelArr.length; i++){
const {x,y} = pointer;
this.drawRect(y * this.boxSize,x * this.boxSize,this.boxSize,this.boxSize,this.levelArr[i - 1]);
pointer.y += 1;
if(i % levelNum === 0){
pointer.x += 1;
pointer.y = 0;
}
}
}
drawRect(x,y,w,h,text){
this.ctx.beginPath();
if(text === 'zzz'){
this.ctx.fillStyle = "rgba(255,255,255,1)";
}else{
this.ctx.fillStyle = "rgba(0,0,0,0.5)";
}
this.ctx.fillRect(x,y,w,h);
if(text!== 'zzz'){
this.ctx.fillStyle = "rgba(255,255,255,1)";
this.ctx.font = "18px Arial";
this.ctx.fillText(text,x + w / 2,y + h / 2);
}
this.ctx.strokeRect(x,y,w,h);
this.ctx.closePath();
}
4. 添加点击事件
这里我们需要修改margin变量,在click方法中计算鼠标点击在canvas中的位置。
judgeMove方法中我们判断是否可以移动并移动
在这个判断中+1判断时的临近的,解决了左右移动,
看到图中8想要移动下来,1移动上去怎么判断呢
这是一维数组格式化后展示
[
4,8,7,
2,z,6,
3,1,5
]
可以看到8的索引加上3就是z的索引,但是发现好像就是加上了每列的数量,获取到了此信息就可以判断了,假设索引等于4,判断4+3等不等于z的索引如果等于就可以移动
这样子基本功能就完成了。
// 获取边距
getMargin() {
const {offsetLeft,offsetTop} = this.cvs;
this.margin = {left:offsetLeft,top:offsetTop};
}
addClick(){
this.cvs.addEventListener("click",this.click);
}
// 处理点击事件
click = (e) => {
const {clientX,clientY} = e;
const x = clientX - this.margin.left;
const y = clientY - this.margin.top;
this.getClickBox(x,y);
}
// 获取点击的方块
getClickBox(x,y) {
this.judgeMove(this.getIndex(x,y));
}
// 判断点击的方块是否可移动并移动
judgeMove(obj) {
const {x,y} = obj;
const index = this.levels[this.level] * y + x; // 获取索引
const zzzIndex = this.levelArr.findIndex(item => item === 'zzz');// 获取zzz索引
const current = this.levelArr[index];
const step = this.levels[this.level];
if(current !== 'zzz'){
if(index + 1 === zzzIndex || zzzIndex + 1 === index || index + step === zzzIndex || zzzIndex + step === index){
this.levelArr[index] = 'zzz';
this.levelArr[zzzIndex] = current;
this.draw();
this.judgeSuccess();
}
}
}
// 通过x,y获取方块的索引
getIndex(x,y) {
const obj = {x:'',y:''};
const {left,top} = this.margin;
const arr = Array.from({length: this.levels[this.level]},(_,index) => index * this.boxSize + this.boxSize);
for(let i = 0; i < arr.length; i++){
if(obj.x === '' && x < arr[i]){
obj.x = i;
}
if(obj.y === '' && y < arr[i]){
obj.y = i;
}
}
return obj;
}
5. 所有代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
list-style: none;
}
canvas{
display: block;
margin: auto;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<select name="level" id="select">
<option value="0">简单</option>
<option value="1">中等</option>
<option value="2">困难</option>
</select>
<script>
class Game {
constructor() {
this.boxSize = 100;
this.cvs = document.getElementById("canvas");
this.ctx = this.cvs.getContext("2d");
// 边距
this.margin = {left:0,top:0};
this.level = 0;
this.levels = [3,4,5];
this.levelArr = [];
this.levelCopy = [];
this.init();
}
init(){
this.cvs.width = this.cvs.height = this.boxSize * this.levels[this.level];
this.ctx.clearRect(0,0,this.cvs.width,this.cvs.height);
this.initLevel();
this.addClick();
}
// 获取边距
getMargin() {
const {offsetLeft,offsetTop} = this.cvs;
this.margin = {left:offsetLeft,top:offsetTop};
}
drawRectLine(x,y,w,h,border) {
this.ctx.beginPath()
this.ctx.strokeStyle = "rgba(0,0,0,0.5)";
this.ctx.strokeRect(x,y,w,h);
this.ctx.closePath();
}
drawRect(x,y,w,h,text){
this.ctx.beginPath();
if(text === 'zzz'){
this.ctx.fillStyle = "rgba(255,255,255,1)";
}else{
this.ctx.fillStyle = "rgba(0,0,0,0.5)";
}
this.ctx.fillRect(x,y,w,h);
if(text!== 'zzz'){
this.ctx.fillStyle = "rgba(255,255,255,1)";
this.ctx.font = "18px Arial";
this.ctx.fillText(text,x + w / 2,y + h / 2);
}
this.ctx.strokeRect(x,y,w,h);
this.ctx.closePath();
}
// 打乱数组
shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
// 设置难度
setLevel(level) {
this.level = level;
this.initLevel();
}
// 关卡
initLevel() {
this.cvs.width = this.cvs.height = this.boxSize * this.levels[this.level];
this.getMargin();
this.levelCopy = [];
this.levelArr = [];
const levelNum = this.levels[this.level];
const count = levelNum * levelNum - 1;
this.levelCopy = Array.from({length: count}, (_, index) => index + 1);
this.levelCopy.push('zzz');
this.levelArr = this.shuffleArray([...this.levelCopy]);
this.draw();
}
// 绘制
draw() {
this.ctx.clearRect(0,0,this.cvs.width,this.cvs.height);
this.drawRectLine(0,0,this.cvs.width,this.cvs.height);
const levelNum = this.levels[this.level];
const pointer = {x:0,y:0};
for(let i = 1; i <= this.levelArr.length; i++){
const {x,y} = pointer;
this.drawRect(y * this.boxSize,x * this.boxSize,this.boxSize,this.boxSize,this.levelArr[i - 1]);
pointer.y += 1;
if(i % levelNum === 0){
pointer.x += 1;
pointer.y = 0;
}
}
}
// 判断是否成功
judgeSuccess() {
const copy = JSON.stringify(this.levelCopy);
const arr = JSON.stringify(this.levelArr);
if(copy === arr){
if(this.level == 2){
alert('你通关了真哇塞');
return;
}else{
alert('恭喜你,过关了');
this.level += 1;
this.initLevel();
}
}
}
// 判断点击的方块是否可移动并移动
judgeMove(obj) {
const {x,y} = obj;
const index = this.levels[this.level] * y + x; // 获取索引
const zzzIndex = this.levelArr.findIndex(item => item === 'zzz');// 获取zzz索引
const current = this.levelArr[index];
const step = this.levels[this.level];
if(current !== 'zzz'){
if(index + 1 === zzzIndex || zzzIndex + 1 === index || index + step === zzzIndex || zzzIndex + step === index){
this.levelArr[index] = 'zzz';
this.levelArr[zzzIndex] = current;
this.draw();
this.judgeSuccess();
}
}
}
// 通过x,y获取方块的索引
getIndex(x,y) {
const obj = {x:'',y:''};
const {left,top} = this.margin;
const arr = Array.from({length: this.levels[this.level]},(_,index) => index * this.boxSize + this.boxSize);
for(let i = 0; i < arr.length; i++){
if(obj.x === '' && x < arr[i]){
obj.x = i;
}
if(obj.y === '' && y < arr[i]){
obj.y = i;
}
}
return obj;
}
// 获取点击的方块
getClickBox(x,y) {
this.judgeMove(this.getIndex(x,y));
}
// 处理点击事件
click = (e) => {
const {clientX,clientY} = e;
const x = clientX - this.margin.left;
const y = clientY - this.margin.top;
this.getClickBox(x,y);
}
// 添加点击事件
addClick(){
this.cvs.addEventListener("click",this.click);
}
}
let game;
game = new Game();
const select = document.getElementById('select');
select.onchange = function(){
game.setLevel(this.value);
}
</script>
</body>
</html>