html5的canvas实现五子棋小游戏
html5也出来也很久了,也是html标记语言的第五次重大修改。html5带来了很多原来没有的新特性,可以通过https://www.w3.org/TR/html5/查看带的特性,目前大部分浏览器都支持html5(IE9以上)。这篇文章我们将使用canvas来实现一个五子棋游戏。canvas
提供了绘制2D和3D(WebGL)的接口, 游戏的图形显示我使用canvas
来实现,逻辑功能还是使用javaScript
来控制。
游戏分析
五子棋的棋盘和围棋一样只是格数多少的问题,本例中我们使用围棋19路这个格局(其实就是一个19*19的矩阵)。通过格子的大小来绘制线条,棋子的直径就是格子的大小,使用棋手对象来代表棋子(本例我们新建两个棋手对象,通过更新下棋状态来通知棋手下棋)
,使用二维数组(矩阵
)来记录棋手下棋的位置。最终我们还要判断胜负关系, 本例的判断方法是判断当前棋子的上、下、左、右、左上、右下、右上、左下
的八个方向中各自对应的组合方向是否能形成5子
。
创建棋盘
新建一个Desk
类, 类的主要功能就是创建画布和渲染棋盘,以及一些开放功能获取配置、获取画布对象。
function Desk(standard, options) {
var defaults = _.merge({
size: 50,
standard: standard
}, options),
width = height = standard * parseInt(defaults.size, 10),
radius = parseInt(defaults.size, 10) / 2,
desk = document.createElement('div'),
/**
*
* @type {HTMLCanvasElement}
*/
canvas = document.createElement('canvas'),
ctx = null;
defaults["width"] = width;
defaults["height"]= height;
defaults["radius"]= radius;
canvas.setAttribute('id', 'deskCanvas');
canvas.setAttribute('width', width + "px");
canvas.setAttribute('height',height + "px");
canvas.style.cursor = "pointer";
desk.style.margin = "10px auto";
desk.style.width = width + "px";
/**
*
* @param key
* @returns {*|null}
*/
this.getOption = function(key) {
return defaults[key] || null;
};
/**
*
* @returns {HTMLCanvasElement}
*/
this.getCanvas = function() {
return canvas;
};
this.getContext= function(driver) {
if (ctx === null) {
return canvas.getContext(driver || "2d");
}
return ctx;
};
this.setContext = function(content) {
ctx = content;
};
desk.appendChild(canvas);
document.body.insertBefore(desk, document.body.firstChild);
}
Desk.prototype = {
draw: function() {
/**
*
* @type {CanvasRenderingContext2D|WebGLRenderingContext}
* @see https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D
*/
var ctx = this.getContext(),
offset = 0,
size = this.getOption('size'),
radius = this.getRadius(),
width = this.getOption('width'),
height = this.getOption('height'),
standard = this.getOption('standard');
this.setContext(ctx);
//设置填充颜色
ctx.fillStyle = "#f6f2e9";
//使用fillStyle设置的颜色来填充矩形区域
ctx.fillRect(0, 0, this.getOption("width"), this.getOption("height"));
ctx.strokeStyle = "#999"; //线条颜色
ctx.lineWidth = 1;
ctx.beginPath();
for (var i = 1; i <= standard; i++) {
offset = i * size - size + radius;
//绘制竖线
ctx.moveTo(offset, radius);
ctx.lineTo(offset, height - radius);
//绘制横线
ctx.moveTo(radius, offset);
ctx.lineTo(width - radius, offset);
}
ctx.stroke();
ctx.closePath();
},
getRadius: function() {
return this.getOption("radius");
},
getSize: function() {
return this.getOption("size");
}
};
我们只需要new Desk(19).draw()
就可以渲染出一个棋盘。
Note::
坐标点(x, y)的值不能从0,0开始,要从棋子的半径位置开始增加,写在边界棋子会绘制不全。
棋子绘制
新建一个Chess
类,实现绘制棋子功能,初始化矩阵,获取坐标, 计算游结束。
function Chess(desk) {
var position = [];
this.standard = desk.getOption('standard') - 1;
this.size = desk.getSize();
this.desk = desk;
for (var y = 0; y <= this.standard; y++) {
position[y] = [];
for (var x = 0; x <= this.standard; x++) {
position[y][x] = null;
}
}
this.addPosition = function(object) {
var x = Math.floor(object.point.x / this.size),
y = Math.floor(object.point.y / this.size);
if (position[y][x] !== null) {
return false;
}
position[y][x] = object;
return true;
};
this.getPosition = function() {
return position;
};
this.ctx = this.desk.getContext();
this.radius = this.desk.getRadius();
}
Chess.prototype = {
/**
* 绘制棋子
* @param player
* @returns {*}
*/
draw: function(player) {
var point = this.roundCirclePoint(player.getXY());
if (typeof point[0] === "object") {
return this.noticeReact(point);
}
var object = {player: player, point: point};
if (!this.addPosition(object)) {
return null;
}
this.ctx.beginPath();
this.ctx.fillStyle = player.color;
this.ctx.arc(point.x, point.y, this.radius, 0, 2 * Math.PI);
this.ctx.fill();
this.ctx.closePath();
if (this.over(object)) {
alert("玩家:" + player.name + "获得胜利");
}
return point;
},
/**
* 绘制提示点
* @param points
*/
noticeReact: function(points) {
this.ctx.strokeStyle = "#f4645f";
for (var i in points) {
var m = points[i];
this.ctx.strokeRect(
m.x - (this.radius / 2), m.y - (this.radius / 2), this.size - this.radius, this.size - this.radius
);
}
return null;
},
/**
* 转换坐标
* @param point
* @returns {*}
*/
roundCirclePoint: function(point) {
var x = Math.floor((point.x - this.radius) / this.size),
y = Math.floor((point.y - this.radius) / this.size),
circle = [];
if (x < 0) x = 0;
if (y < 0) y = 0;
//查找附近四个圆点中心
for(var i = x; i <= x+1; i++) {
for(var j = y; j <= y+1; j++) {
circle.push({
x: i * 50 + this.radius,
y: j * 50 + this.radius
});
}
}
for (var index in circle) {
var m = circle[index];
var distance = Math.pow(m.x - point.x, 2) + Math.pow(m.y - point.y, 2);
//计算点是否在圆内
if (distance < Math.pow(this.radius, 2)) {
return m;
}
}
return circle;
},
/**
* 判断游戏结束
* 通过判断当前棋子的八向连续相同的四子,或者正反方向的连续相同的棋子的和大于四
* @param object
* @returns {boolean}
*/
over: function(object) {
var player = object.player,
point = object.point,
x = Math.floor(point.x / this.size),
y = Math.floor(point.y / this.size),
position = this.getPosition(),
hx = vy = sh = sv = _hx = _vy = _sh = _sv = 0;
for (var i = 1; i <= 4; i++) {
if (x+i < this.standard && position[y][x+i] && position[y][x+i].player === player && (i - hx) === 1) {
hx++;
}
if (y+i < this.standard && position[y+i][x] && position[y+i][x].player === player && (i - vy) === 1) {
vy++;
}
if (y+i < this.standard && x+i < this.standard && position[y+i][x+i] && position[y+i][x+i].player === player && (i - sv) === 1) {
sv++;
}
if (y+i < this.standard && x-i > -1 && position[y+i][x-i] && position[y+i][x-i].player === player && (i - sh) === 1) {
sh++;
}
}
for (var i = -1; i >= -4; i--) {
if (x+i > -1 && position[y][x+i] && position[y][x+i].player === player && (i + _hx) === -1) {
_hx++;
}
if (y+i > -1 && position[y+i][x] && position[y+i][x].player === player && (i + _vy) === -1) {
_vy++;
}
if (y+i > -1 && x+i > -1 && position[y+i][x+i] && position[y+i][x+i].player === player && (i + _sv) === -1) {
_sv++;
}
if (y+i > -1 && x-i < this.standard && position[y+i][x-i] && position[y+i][x-i].player === player && (i + _sh) === -1) {
_sh++;
}
}
return hx + _hx >= 4 || vy + _vy >= 4 || sv + _sv >= 4 || sh + _sh >= 4;
}
};
玩家
下棋肯定少不了玩家,这里我们通过玩家点击画布获取位置再调用棋子类绘制玩家的棋子,同时更新玩家的状态。默认玩家都是等待开始的状态,当玩点击的时候更新为下棋的状态,下完的时候更新其它玩家的状态同时更新自己的状态。
/**
*
* @param name
* @constructor
*/
function Player(name, color, chess) {
this.name = name;
this.color = color;
this.state = 'waitStart';
var x = y = 0;
this.setXY = function(xv, yv) {x = xv; y = yv;};
this.getXY = function() {return {x: x, y: y};};
this.chess = chess;
}
Player.prototype = {
/**
*
*/
playChess: function() {
if (this.state === 'waiting' || this.state === 'waitStart' || this.state === 'waitPlay') {
return ;
}
var circlePoint = this.chess.draw(this);
if (circlePoint !== null) {
Events.trigger("notify", ctrl, this);
} else {
this.updateState("waitPlay");
}
},
updateState: function(state) {
this.state = state;
},
getState: function() {
return this.state;
}
};
Note:
this.updateState("waitPlay")
当前用户点击的区域无法匹配到正常的圆点,把玩家的状态更新为等待玩的状态。
控制
控制类主要是针对玩家的,获取当前要下棋的用户,通知回调。
function Control() {
this.player = [];
};
Control.prototype = {
addPlayer: function(player) {
this.player.push(player);
},
getPlaying: function() {
for (var i in this.player) {
var state = this.player[i].getState();
if (state === 'waitPlay') {
return this.player[i];
}
}
var player = this.player[0];
player.updateState("waitPlay");
return player;
},
notify: function(player) {
for (var i in this.player) {
if (this.player[i] !== player) {
this.player[i].updateState("waitPlay");
} else {
this.player[i].updateState("waiting");
}
}
},
getPlayer: function() {
return this.player;
}
};
通过以上的几个类我们就完成了从棋盘到玩家再到下棋的整个流程。
事例
var desk = new Desk(19);
var chess = new Chess(desk);
var playerOne = new Player("Tom", "#FFF", chess);
var playerTwo = new Player("Seven", "#000", chess);
var ctrl = new Control();
ctrl.addPlayer(playerOne);
ctrl.addPlayer(playerTwo);
desk.draw();
Events.on('click', function(e) {
var player = ctrl.getPlaying();
player.updateState("playing");
player.setXY(e.offsetX, e.offsetY);
player.playChess();
}, desk.getCanvas());
Events.on("notify", function(player) {
ctrl.notify(player);
});
总结
在整个五子棋代码中实际是没有难点之处的,主要还是个人思路要清晰。大家也可以给我指出不足的地方。下期我们使用这个和websocket
做一个在线的五子棋游戏。
源代码
https://github.com/TianLiangZhou/loocode-example/tree/master/gobang
推荐阅读
- https://www.w3.org/TR/html5/
- https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
- https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
- http://www.html5gamedevs.com/
- http://bucephalus.org/text/CanvasHandbook/CanvasHandbook.html
- https://joshondesign.com/p/books/canvasdeepdive/chapter01.html