看到跳棋游戏,一个2到6人可一起玩的游戏,于是联想起,自己上小学时候陪同学们玩过的弹珠游戏,是不是跟跳棋游戏很像呢,看了跳棋玩法,有兴趣就研究,这里把跳棋游戏给弄出来了,想知道地图怎么画,对此感兴趣的同学可以研究研究,让家人孩子们一起参与游戏互动吧,珍惜陪伴的幸福时光。
打开微信开发工具,选择小程序,创建一个项目,
例如项目名称为
miniprogram_chinese_draughts
,然后选择以下,再确定创建
- AppID 使用自己的测试号
- 不使用云服务
- JavaScript - 基础模板
开始页
新建小程序项目后,开发工具会自动生成一些基础模板文件,
在位置/pages/index/index
这里,把index.wxml
改成开始页面,添加一个选择人数的表单,和按钮,显示如下
跳棋游戏中的是不能有5个人参与的,因为第5个参与者没有对手,不公平吧,
把index.js
这里修改一下,添加一个方法enterGame(e)
,点击开始游戏会调用到,代码如下,
enterGame(e){
const { count } = e.detail.value;
wx.navigateTo({
url: '/pages/game/game?args='+count,
})
}
跳转到游戏页面,同时会传一个参数
count
表示参与人数
游戏页面
添加一个游戏页面,文件位置在pages/game/game
,页面game.wxml
里的添加布局,内容如下
<view class="page">
<canvas class="canvas" id="canv" type="2d" bindtouchstart="onTouchStart"></canvas>
<scroll-view class="scroll-view" scroll-y="true">
<view class="game-info">
<!-- 这里显示游戏状态信息的布局 --->
</view>
</scroll-view>
</view>
其中
canvas
画布,是最重要的组件,可用来绘制画面,
另外放置的组件是用于显示每个参与选手(玩家)的游戏状态,
初始化
开始页有传来的参数count,就从游戏页面onLoad(options)
加载方法里获取,代码如下
/**
* 页面的初始数据
*/
data: {
players:[],//参与的玩家列表
currentPlayer:0,//指定哪个玩家下棋
},
onLoad(options) {
let index = 0;//默认的选择
//如果从上一页有传来参数,这里改变默认的选择
if(options?.args) index = ['2','3','4','6'].indexOf(options.args);
//按照选择人数分配玩家
let players = [[0,5],[1,2,5],[1,2,3,4],[0,1,2,3,4,5]][index].map(id=>{
return {
id: parseInt(id),//玩家的id
color: ChessColors[id],//对应的颜色
step: 0,//开始步数
rank: 0,//用于记录的排名
}
});
//更新玩家列表显示
this.setData({
players
})
},
接下来,从游戏页面的准备完成事件onReady()
方法里,
获取一下页面布局中的canvas
,然后绑定触摸开始事件,代码如下
onReady() {
wx.createSelectorQuery().select('#canv').fields({
size:true,
node:true
},res=>{
//...
//获取画布数据
this.canvasData = {
canvas:res.node,//画布canvas元素的节点
ctx:res.node.getContext('2d'),//画布canvas的绘制工具
};
//实例化地图对象
const map = new Map(this.canvasData,this.data.players.map(p=>p.id));
//绘制地图的异步方法
map.drawMap(()=>{
//绘制完成了,这里绘制所有参与玩家的棋子
map.drawChesses();
});
this.map = map;
//...
}).exec()
},
onTouchStart(e) {
//...这里是canvas画布触摸开始的处理方法
}
从上面看,其中的Map
是指地图模块,
在使用模块前,需要在代码文件头写一行导入模块的代码,也就是引用,代码如下
import { Map, ChessColors } from '../../utils/map.js';
还有
ChessColors
,是默认分配玩家的棋子颜色集合,若想自定义,可在此修改,
模块文件代码共有240行(包括空格行),看着不多,实现了地图数据和地图绘制方法
游戏背景
在上面的初始化逻辑里,游戏背景已经绘制好了,
地图的绘制方法是drawMap(callback)
,参数callback
是传入的回调方法,绘制完成后会调用,
这里简单说一下它的实现过程,代码如下
drawMap(callback){
const { canvas, ctx } = this.canvasData;
//定义它们分别是网格,所有棋子,格子大小,格子半径,圆半径
const { grids, chesses, gSize, gR, gRp } = this;
//初始化值
const angle = Math.PI*2;
//修改设置
ctx.strokeStyle=BorderColor;
grids.forEach((g,i)=>{
//这个方法是扫描一个格子位置中相邻的一些格子(圆)
let gs = this.scanGrids(g);
if (gs.length<=0) return;
//在这里画上一些格子之间的连接线
gs.forEach(g1=>{
ctx.beginPath();
ctx.moveTo(g.x,g.y);
ctx.lineTo(g1.x,g1.y);
ctx.stroke();
});
});
ctx.fillStyle=BorderColor;
//接下来,画出所有的格子(圆),就是覆盖着画
grids.forEach((g,i)=>{
//...
ctx.beginPath();
ctx.arc(g.x,g.y,gRp,0,angle);
ctx.fill();
ctx.stroke();
});
//这里就算画好了,导出图像
let img = canvas.createImage();
img.onload = () => callback(img);
img.src = canvas.toDataURL();
//网格地图,需要设置到bgImg缓存起来(背景图),将来有用
this.bgImg = img;
}
由于这里是设置到
bgImg
缓存中,每次更新就要重新绘制出来,
也可以将绘制的图像设置到背景图中,每次更新就不用重新绘制地图,直接在底层显示,
然后绘制其它的,例如绘制改变位置后的棋子
选手和棋子
绘制不同选手阵营中的棋子,也可以看作是珠子,不同选手代表的棋子和格子颜色不同,
如果棋子的位置改变后,就需要调用地图的方法drawChesses(selectIndex)
重新绘制,
显示最新的所有棋子布局,代码如下
drawChesses(selectIndex){
const { canvas, ctx } = this.canvasData;
const { grids, chesses, gSize, gR, gRp } = this;
const angle = Math.PI*2;
//每次绘制前,都要清空画布
ctx.clearRect(0,0,canvas.width,canvas.height);
//将缓存的背景图重新绘制出来
ctx.drawImage(this.bgImg, 0, 0);
//然后,再去绘制所有的棋子
let isSelect=false;
chesses.forEach(chess=>{
//判断是否有选择,记录一下
if(chess.index==selectIndex) isSelect=true;
//如果有选择棋子,就在棋子周围画一个圆(代表选择)
this.drawChesse(chess,selectIndex);
});
//这里还要判断,如果没有棋子被选择过,那剩下可能是选择了没有棋子占用的格子
if(!isSelect) {
this.drawChesse(undefined,selectIndex);
}
}
从上面看出,传入的参数
selectIndex
就是指玩家选择的格子索引,
同时也传给了方法drawChesse(chess,selectIndex)
,另一个参数chess
就是指选择的棋子,没有选择就传undefined
,这个方法实现了怎样绘制网格中的棋子
讲到这里,游戏页面就算绘制好了,编译运行看效果,
显示出来是下图这样的,一共6个玩家参与的跳棋游戏
游戏逻辑
从画布的触摸开始事件中去处理,就是上面提到的onTouchStart(e)
方法,
游戏规则
这里再想一下游戏逻辑,当用户点击棋子,选择自己的棋子后,接下来应该怎样实现呢,
这里了解以下游戏规则,
按照游戏规则,理清一下实现思路:
- 判断每个玩家的步数,在30步内未走出自己的阵营的,就判出局,不能再继续;
- 走棋,有两个走法:第一个就是只走一步;第二个,就是以旁边的棋子为方向,跳到棋子方向的另一边,可连续跳;
- 步数,走一步算一步,跳一次也算一步,如果是连续跳也算一步,可以修改过来的;
- 将对方阵营所有的棋子空位全部占领就判赢,如果是三人以上的,赢的按排名计算,剩下的判出局;
怎么实现游戏逻辑的呢,实现游戏逻辑是最复杂的地方吧,说来也是,只要理解它的实现过程基本就可以自己尝试了,
选择棋子
第一步,要选择棋子,然后移动棋子,就在它的触摸事件中处理,代码如下
onTouchStart(e) {
//...
const touch = e.touches[0];//第一个触摸点(位置)
const { grids, gRp, chesses } = this.map;
//所有玩家,指向当前下棋的玩家
const { players, currentPlayer } = this.data;
//通过触摸点位置判断,从网格中找出选择的棋子
let chess;
let grid = grids.find(grid=>{
//先从网格位置判断...如果是在这个格子里,再判断是否有棋子在这个格子上...
});
//没有点到格子的画,就返回不处理了
if (grid==undefined) return;
//给出当前在下棋的玩家
const player = players[currentPlayer];
let { selectGridData } = this;
//判断之前是否选择过网格(棋子)了,还有就是选择的棋子是当前在下棋的玩家的,如果条件满足了,反之就是不满足
if (!(selectGridData?.chess?.id==player.id)) {
//不满足以上条件,就重新设置选择数据
this.selectGridData = { grid, chess, moveCount:0 };
//重新绘制所有棋子
this.map.drawChesses(grid.i);
//不再继续处理
return;
}
//如果选择到棋子
if (chess) {
//不是自己的,提示一下
if (chess.id!=player.id) {
this.showToast(`请${player.id+1}号玩家操作`);
return;
}
//如果是相邻跳过的,又选择下一个棋子,就不是连续跳了
if (selectGridData.moveCount>1) {
//换下一个玩家
this.setNextPlayer();
return;
}
//剩下的,只有选择棋子了吧,直接给它更新显示
Object.assign(this.selectGridData, { chess, grid });
this.map.drawChesses(grid.i);
return;
}
//获得移动到下一个位置的距离,相对格子的,一个格子算一个单位距离
let count = this.map.gotoGrid(selectGridData.grid,grid);
if (count>0) {
//判断是否只移动1个(走),然后就是上次移动的是否大于1个(跳),满足条件
if (!(count==1 && selectGridData.moveCount>count)) {
//反之不满足,这里更新选择的棋子位置
chess = selectGridData.chess;
chess.index = grid.i;
//记录移动距离
selectGridData.moveCount = count;
//棋子步数加1
player.step++;
//获取玩家在自己阵营内的所有棋子
let chesses2 = this.map.getPlayerChessesAtGrids(player.id);
//判断这个玩家是否在游戏,还有步数
if (player.rank==0 && player.step==MinStep) {
//步数大于MinStep限制,还未走出自己阵营,就得通知这个玩家出局了...
}
//更新显示玩家的状态
this.setData({
players
});
}
}
//若只移动1个(走),就更新绘制棋子,换下一个玩家
if (count==1){
this.map.drawChesses(grid.i);
this.setNextPlayer();
return;
}
//剩下的,就是其它操作了,直接更新显示即可
Object.assign(this.selectGridData, { chess, grid });
this.map.drawChesses(grid.i);
},
看上面,就会注意到,还有一些细节都在地图模块的方法里了,
上面还有个重要的方法setNextPlayer()
,是处理换下一个玩家来,切换玩家前还要加个判断,
就是判断玩家输赢,最后若是剩下一个玩家还没赢的话,那么游戏就结束,代码如下
setNextPlayer(){
const { players, currentPlayer } = this.data;
//定义当前参与的玩家
const player = players[currentPlayer];
//找出当前玩家的所有棋子
let chesses2 = this.map.getPlayerChessesAtGrids(player.id);
//计算当前玩家是否全部占领对方的阵营
let isWin = chesses2.reduce((current,next,index)=>{
//棋子flag属性判断即可...
});
if (isWin) {
let rank=0;
//全部占领了,更新排名,不再参与...
player.rank = rank+1;
//可通过排名和参与人数判断是否到最后了
if (player.rank+1 >= players.length) {
//这里是最后的了,直接弹出游戏结束...
this.showGameOver(`${player.id+1}号玩家赢了,成功占领对方的阵营`);
return;
}else{
//提示一下,然后继续
this.showModal(`${player.id+1}号玩家赢了,成功占领对方的阵营`);
}
}
//换下一个玩家
let current = (currentPlayer+1)%players.length;
//此处省略了...
this.setData({
currentPlayer: current,
players
});
this.showToast(`请${players[current].id+1}号玩家操作`);
},
游戏测试
就讲到这里,跳棋游戏的实现思路大概清晰了吧,
调用的其它方法不对,虽然没有讲,但是,见其名知其意,相信自己可以实现出来的,
来看一下,游戏运行的效果动图如下,这是两个人参与的,测试完感觉没问题,
想看项目源码的,请点此查看,找到资源类别一栏(如果是手机上看可能找不到,就换成电脑浏览器访问 ),
资源列表里面,找到其中的跳棋游戏项目源码,请放心下载,感谢支持。
游戏启示:对此你有什么启发呢
- 选好目标,尽快占领,类似五行斗法里的相生相克,
- 在这个游戏规则里,尽可能快速移动,落后就要挨打