用js写卡牌游戏(二)
又废话(前言)
别看这个游戏现在这个垃圾样,我可是摸索了将近半年才写出来的,所以我现在理解一个好游戏要是想做出来,为啥要两三年了。
目前最新进度游戏线上地址 ,欢迎大家注册了之后体验(如果没有人对战,可以单人剧情体验),希望大家多提建议!
第二回合(对战通信)
采购完项目的材料,要开始建地基了。
如果把我们的游戏中的概念日常化,那么对战其实就是两个人进入了一个聊天室(匹配),用我指定的语言(卡牌)在聊天(对战)。所以我们先按照聊天室来编写我们的程序。
首先先给两个人开个房,emmmm……正规房。
作为一个在线对战游戏,需要保证两个人能连到一个房间,并且不掉线,掉线的时候自动重连,听起来这一步就很费功夫了,好在这些需求socket.io都已经实现了,我们就聚焦在怎么实现游戏系统上就ok。进入房间下一步就开始进行广播,当我发送一条消息的时候,由服务器接收这个消息,然后进行处理后再广播给包括我在内的房间里的用户。
这个地方要解释一下为什么要广播给自己,而不只广播给别人在本地处理自己。这个地方我是这么考虑的:
1. 能够统一的处理所有人的同类消息。
2. 游戏是时序重要的,也就是说,每张牌一定要按照打出的顺序处理,不然会有截然不同的结果。
3. 不能在客户端本地做操作,在服务器上做统一的处理以防止本地作弊。
定一个初步的目标是两个人能够进入房间并且同步增加一个计数器。
客户端的连接可以直接使用之前的代码,先做服务端的处理,将恰好两人放入同一个房间。
这里我使用的方法是先使用一个数组保存待匹配的用户,一个缓存保存每个用户当前的房间号码,再使用一个缓存保存每个房间里的游戏数据。在app.js中插入下列代码:
const waitPairQueue = []; // 等待排序的队列
const memoryData = {}; // 缓存的房间游戏数据,key => 房间号,value => 游戏数据
const existUserGameRoomMap = {}; // 缓存用户的房间号, key => 用户标识,value => 房间号
这样,当某个用户加入进来的时候,我们进行下列操作:
- 对用户先发送连接成功请等待的命令。
- 查看是否有等待匹配的用户,没有则将当前用户加入队列,如果有则取出队列中的一个用户。
- 生成一个房间号码,将用户的信息缓存起来,将通过房间号初始化游戏数据。
- 用户连入同一个socket服务,并且缓存用户的socket实例。
- 告诉用户连接完成,同时发送初始化游戏数据。
在原connection事件里,新监听一个连接事件。
socket.on('CONNECT', function () {
let args = Array.prototype.slice.call(arguments); // 将arguments转为真数组
const {userId} = args;
});
在这个连接事件里,进行上面列出的5步操作:
socket.emit("WAITE"); // 不管三七二十一,先给老子等起
在匹配队列里寻找对手,为了简单,我暂时先直接取队列里的第0个,以后会完善一套科学的匹配机制。
if (waitPairQueue.length === 0) { // 如果当前没有等待的玩家,则将自己加入等待队列
waitPairQueue.push({
userId, socket
});
} else {
let waitPlayer = waitPairQueue.splice(0, 1)[0]; // 随便拉个小伙干一架
// 下一步从这继续
}
我决定用uuid来作为房间号码,所以需要安装一个uuid库,执行:
npm i uuid --save
记得引入uuid:
const uuidv4 = require('uuid/v4');
在一局游戏中,玩家需要标识在这局游戏中的唯一身份,我就用one和two来表示了。
let roomNumber = uuidv4(); // 生成房间号码
// 初始化游戏数据
waitPlayer.roomNumber = roomNumber;
memoryData[roomNumber] = {
"one": waitPlayer,
"two": {
userId, socket, roomNumber
},
count: 0
};
existUserGameRoomMap[userId] = roomNumber;
existUserGameRoomMap[waitPlayer.userId] = roomNumber;
socketio加入房间使用join方法:
// 进入房间
socket.join(roomNumber);
waitPlayer.socket.join(roomNumber);
初始化游戏数据,我们还没有设计具体的游戏数据结构,就使用个简单的计数器先来做测试。
// 游戏初始化完成,发送游戏初始化数据
waitPlayer.socket.emit("START", {
start: 0,
memberId: "one"
});
socket.emit("START", {
start: 0,
memberId: "two"
});
同时我们还需要监听客户端的操作,比如增加计数器,增加一个事件ADD,处理count的增加后再广播给所有用户:
socket.on("ADD", function() {
let args = Array.prototype.slice.call(arguments);
let roomNumber = existUserGameRoomMap[args.userId];
memoryData[roomNumber].count += 1;
memoryData[roomNumber]["one"].socket.emit("UPDATE", {
count: memoryData[roomNumber].count
});
memoryData[roomNumber]["two"].socket.emit("UPDATE", {
count: memoryData[roomNumber].count
});
})
完整的代码如下:
socket.on('CONNECT', function () {
let args = Array.prototype.slice.call(arguments); // 将arguments转为真数组
const {userId} = args;
socket.emit("WAITE"); // 不管三七二十一,先给老子等起
if (waitPairQueue.length === 0) {
waitPairQueue.push({
userId, socket
});
socket.emit("WAITE");
} else {
let waitPlayer = waitPairQueue.splice(0, 1)[0]; // 随便拉个小伙干一架
let roomNumber = uuidv4(); // 生成房间号码
// 初始化游戏数据
waitPlayer.roomNumber = roomNumber;
memoryData[roomNumber] = {
"one": waitPlayer,
"two": {
userId, socket, roomNumber
},
start: 0
};
existUserGameRoomMap[userId] = roomNumber;
existUserGameRoomMap[waitPlayer.userId] = roomNumber;
// 进入房间
socket.join(roomNumber);
waitPlayer.socket.join(roomNumber);
// 游戏初始化完成,发送游戏初始化数据
waitPlayer.socket.emit("START", {
start: 0,
memberId: "one"
});
socket.emit("START", {
start: 0,
memberId: "two"
});
}
});
socket.on("ADD", function() {
let args = Array.prototype.slice.call(arguments);
let roomNumber = existUserGameRoomMap[args.userId];
memoryData[roomNumber].count += 1;
memoryData[roomNumber]["one"].socket.emit("UPDATE", {
count: memoryData[roomNumber].count
});
memoryData[roomNumber]["two"].socket.emit("UPDATE", {
count: memoryData[roomNumber].count
});
});
接下来就是处理前端的响应了,首先在接收到WAIT的时候,需要显示匹配中,在接收到START的时候,初始化游戏并且把我们的计数器显示在页面上。
首先在data里添加三个变量,一个用作匹配窗口是否显示,一个作为计数器,一个作为暂时的用户id(后续会实现用户系统再进行替换):
data() {
return {
matchDialogShow: false,
count: 0,
userId: new Date().getTime()
};
},
更改mounted方法,添加连接成功后通知服务器匹配,再原来的事件监听删除,换成下面三个:
this.socket.emit("COMMAND", {
type: "CONNECT",
userId: this.userId
});
this.socket.on("WAITE", args => {
this.matchDialogShow = true;
});
this.socket.on("START", args => {
this.count = args.start;
this.matchDialogShow= false;
});
this.socket.on("UPDATE", args => {
this.count = args.count;
});
在页面上添加匹配对话框dom,并且添加计数器显示和计数器增加按钮,同时添加一点样式:
<template>
<div class="app">
<div class="table">
<div class="other-card-area">
</div>
<div class="my-card-area">
{{count}}
</div>
</div>
<div class="my-card">
<button @click="add">+1</button>
</div>
<div class="match-dialog-container" v-show="matchDialogShow">
正在匹配,请等待
</div>
</div>
</template>
.match-dialog-container {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
background: rgba(0, 0, 0, 0.5);
color: white;
}
添加一个方法add,用于发送增加事件给服务器:
methods: {
add() {
this.socket.emit("ADD", {
userId: this.userId
});
}
}
将代码跑起来,打开两个浏览器,点击按钮,就可以看到同一个房间的用户可以控制同一个计数器了。
在这个浏览器点击4下,在另外一个浏览器点击4下
游戏的基本数据交互方法已经实现了,接下来要设计一下游戏的卡牌了,采用卡牌游戏的经典设计:
那么一张卡牌的最基础的数据结构应该如下:
字段 | 描述 |
id | 卡牌的唯一id |
name | 卡牌的名称 |
cardType | 卡牌类型,如:伙伴,魔法效果等 |
cost | 卡牌费用 |
content | 卡牌描述 |
attack | 卡牌攻击 |
life | 卡牌生命 |
游戏的背景,就选择编程界,一来是熟悉,二来目前文章前阅读的大家也都能懂,那我们先来设计一张最简单的卡牌吧。最近互联网寒冬,那就设计一个被开除的程序员吧:
{
id: 1,
name: "被开除的员工",
cardType: 1,
cost: 3,
content: "",
attack: 4,
life: 4
}
接下来,就要使用这个数据制作卡牌了,那就留到下一章吧~
再提一次,目前最新进度游戏线上地址 ,欢迎大家注册了之后体验(如果没有人对战,可以单人剧情体验),希望大家多提建议!
下一章或许我会把三章一起开始录制视频(取决于我懒不懒)