上期内容:
上回已经完成了棋盘、线框、棋子的绘制,以及如何计算绘制的位置信息。本次内容将分享这个游戏的实质,数据结构,以及各个对象功能,以及一些对象依赖关系处理的思想。
代码分享:https://github.com/a74946443/chess
一、游戏的理解
游戏可以说是一个很复杂的工程,我本身不是游戏行业,所以只是简单解释一下。
游戏实质是由人或电脑控制数据,每发生数据变化就重新渲染一次游戏画面(比如图形位置发生变化,图形的有无,图形的颜色等等)。刷新的频率越高,游戏的画面就越细腻,看起来越舒服。
游戏动画,动画是一帧一帧连续变化的图形或图片,每秒需要超过24帧,由于人眼视觉的原因才使得每帧的图形平滑过度,不会出现闪烁。
那么游戏简单说就是由控制、数据和动画形成的一个组合体。
回到这里做的五子棋上,我不需要多余的各种复杂的系统,相比之下就要简单很多了,上期完成了图形渲染方法只需要考虑如何控制落子与判定胜负就可以了。
二、数据结构
考虑五子棋的特征,被控制者是棋子,控制者是玩家,所以棋子是游戏中的主体数据,棋子要依托于棋盘之上,存在边界,多行多列位置固定的结构,可以想到的就是二维数组。
棋盘格与棋子位置是一一对应的,所以需要将棋盘格线的绘制,与棋子位置统一使用二维数组来作为底层数据进行绘制。
接着在上次的Plate对象利用边长创建二维数组
function Plate(){
...
//初始化矩阵 v: vertical 垂直 h: horizontal 水平
let initMatrix = function () {
for (let v = 0; v < chessNumOneSide; v++) { //垂直
cellMatrix[v] = [];
for (let h = 0; h < chessNumOneSide; h++) { //水平
cellMatrix[v][h] = null;
}
}
};
let init = function() {
//矩阵初始化之前要先初始化属性值
...
initPanelAttr(); //初始化棋盘属性
initCxt(); //初始化canvas环境
initMatrix(); //本次在初始化函数中增加初始化矩阵
renderPlate(); //根据矩阵绘制棋盘
}
let clearAllChessDraw = function () {
initMatrix(); //清除棋子时将矩阵数据清除
//清除cxtChess2d整个画布矩形区域
};
init();
}
有了底层的数据结构,且与棋盘线交点一一对应,那么可想而知,游戏中落子就是在矩阵中指定位置上增加了一个标记,每次数据发生变化后就重新渲染一次棋盘。
三、落子重绘
落子就是在矩阵中的指定行列赋一个特定值,触发重新渲染图形,将棋子绘制到棋盘指定位置。
比如矩阵初始化时每一个位置都是0,代表无落子,1代表白方落子,2代表黑方落子。
function Plate() {
...
//修改上一期绘制一个棋子的方法,向矩阵中插入指定值
this.renderOneChess = function(v,h,color,num){
cellMatrix[v][h] = num;
drawOneChess(v,h,color);
};
/**
* 渲染全部存在的棋子
*/
this.renderAllChess = function () {
for (let v = 0; v < cellNumOneSide; v++) {
for (let h = 0; h < cellNumOneSide; h++) {
let flag = cellMatrix[v][h];
if (flag === 1) {
drawOneChess(v, h, '#fff' );
} else if (flag === 2) {
drawOneChess(v, h, '#000' );
}
}
}
};
...
}
<script>
var p = new Plate();
//初始化矩阵后,设置了 (1,1)和(2,2)位置分别是黑子和白子的标记
p.renderOneChess(1,1,'#000', 2);
p.renderOneChess(2,2,'#fff', 1);
// p.renderAllChess(); // 设置矩阵值后,同样会绘制出相应的棋子
</script>
如图:通过矩阵标记渲染全部棋子
棋盘对象的基础功能都完备了,但是依然比较简陋,假设我在渲染棋子时手误把标记1和颜色#000一起传入渲染方法,这样不就产生bug了么!好在第一篇文章中已经分析过,利用对象以及成员属性来确定。
四、构造对象
棋子对象:有颜色和从属玩家两个属性。颜色因为只存在黑白两种颜色,所以将其定义为常量。
/// file:const.js 定义常量 ///
const CAMP_WHITE = 1;
const CAMP_BLACK = 2;
const TYPE_HUMAN = 1;
const TYPE_AI = 2;
const COLOR_MAP = {
1: '#fff',
2: '#000',
};
定义玩家对象,拥有名称、类型、阵营等属性
/// file: player.js ///
/**
* 玩家
* @constructor
*/
function Player(config) {
this.name = config['name']; //玩家名称
this.type = config['type'] || TYPE_HUMAN; //玩家类型
this.camp = config['camp']; //玩家阵营
}
定义棋子对象
/// file: chess.js ///
function Chess(player) {
this.belong = player;
this.color = COLOR_MAP[this.belong.camp];
}
定义控制者
/// file: ctrl.js ///
function Controller(config) {
let allPlayer = config['all']; //注入全部玩家
let curPlayer; //当前有控制权限的玩家
let defaultIdx = 0;
/**
* 切换控制
*/
this.changePlayer = function () {
defaultIdx = +!defaultIdx;
curPlayer = allPlayer[defaultIdx];
};
/**
* 获取玩家
* @returns {*|null}
*/
this.getPlayer = function () {
return curPlayer;
};
/**
* 获取敌人
* @returns {*}
*/
this.getEnemy = function() {
return allPlayer[+!defaultIdx];
};
}
控制者是负责明确对战玩家,并负责交换玩家控制权,以及注册落子事件,对于控制器需要初始化的落子事件在稍后进行定义。
在第一期里面,分析落子和棋子其实是两种对象,落子对象的定义可以说是比较核心的内容。
/// file: runtime.js ///
function Runtime(config) {
this.absPos = config['pos']; // 落子的在矩阵中的位置
this.chess = config['chess']; //棋子对象
this.player = this.chess.belong; // 落子属于的玩家
this.arounds = {}; //棋子周围 8个方向 4条直线上的布局
this.initAround = function (m){
// 计算落子周围棋子
// 判断以当前落子为中心的8条射线组成的布局
// 判断棋子玩家阵营 双人对战只需要记录当前玩家的布局即可
// 计算结果写入this.arounds
};
//每次落子后检测是否满足胜利条件事件
this.judgeWin = function () {
for (let i in this.arounds) {
if (this.arounds.hasOwnProperty(i) && this.arounds[i].length === 5) {
return true;
}
}
return false;
};
}
另外一个核心内容就是事件控制,游戏初始化、落子、切换控制、输赢判定等均是由事件监听控制的。
/// file:event.js ///
/**
* 事件
* @param inject
* @constructor
*/
function PlayEvent(inject = []) { // inject是其他对象的注入
/**
* 创建运行对象
* @param player
* @param idx
* @returns {Runtime}
*/
let createRuntimeObj = function(player, idx) {
let chess = new Chess(player);
return new Runtime({
pos: idx,
chess: chess,
});
};
// 重新开始事件
this.restart = function () {
document.querySelector('#restart').onclick = function (e) {
let plateObj = injectObj[Plate.name]; //棋盘
plateObj.gameInit();
injectObj[App.name].init();
}
};
let putChessDown = function(){
e = e ? event : window.event;
let plateObj = injectObj[Plate.name]; //从注入的对象中获取棋盘对象
let ctrl = injectObj['Controller']; //从注入的对象中获取控制器
let curUser = ctrl.getPlayer(); // 从k控制器获取当前玩家
if (curUser.type === TYPE_HUMAN) {
// 通过鼠标位置获取落子对应的矩阵坐标
let idx = plateObj.getCrossPos(e.clientX, e.clientY);
if (idx) {
//创建落子对象
let runtimeChessObj = createRuntimeObj(curUser, idx);
if (plateObj.renderOneChess(idx.v, idx.h, runtimeChessObj)) {
//初始化周围情况
runtimeChessObj.initAround(plateObj.getMatrix(), false);
if (runtimeChessObj.judgeWin()) {
curUser.win = true;
plateObj.gameOver();
} else {
// 还未胜利时需要交换玩家控制权
ctrl.changePlayer();
}
}
}
}
}
/**
* 下棋事件,
* @param el
*/
this.play = function (el) {
let plateObj = injectObj[Plate.name]; //棋盘
// 注册点击事件 -- 下棋
plateObj.getEventAttach().onclick = putChessDown;
};
let me = this;
let il = inject.length,
injectObj = {};
for (let i = 0; i < il; i++) {
injectObj[inject[i].constructor.name] = inject[i];
if (inject[i].hasOwnProperty('initEvent')) {
inject[i].initEvent(me); //调用各对象内的事件
}
}
}
所有的对象基本都定义好了,那么需要定一个统一入口。
/// file: main.js
function App(config) {
let c = config;
this.init = function () {
//初始化棋盘
let p = new Plate(c);
//玩家1
let firstPlayer = new Player({
name: 'player1',
type: TYPE_HUMAN,
camp: CAMP_BLACK,
});
//玩家2
let secondPlayer = new Player({
name: 'player2',
type: TYPE_HUMAN,
camp: CAMP_WHITE,
});
//控制器 注入控制器,
let ctrl = new Controller({
all: [firstPlayer, secondPlayer],
});
//事件初始化 对象注入事件
let pe = new PlayEvent([p, ctrl, this]);
}
}
到这里整个双人对战的游戏算是构建完成了。