又废话(前言)
别看这个游戏现在这个垃圾样,我可是摸索了将近半年才写出来的,所以我现在理解一个好游戏要是想做出来,为啥要两三年了。
目前最新进度游戏线上地址 ,欢迎大家注册了之后体验(如果没有人对战,可以单人剧情体验),希望大家多提建议!
第二回合(对战通信)
采购完项目的材料,要开始建地基了。
如果把我们的游戏中的概念日常化,那么对战其实就是两个人进入了一个聊天室(匹配),用我指定的语言(卡牌)在聊天(对战)。所以我们先按照聊天室来编写我们的程序。
首先先给两个人开个房,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 }); } }
将代码跑起来,打开两个浏览器,点击按钮,就可以看到同一个房间的用户可以控制同一个计数器了。
游戏的基本数据交互方法已经实现了,接下来要设计一下游戏的卡牌了,采用卡牌游戏的经典设计:
那么一张卡牌的最基础的数据结构应该如下:
字段 | 描述 |
id | 卡牌的唯一id |
name | 卡牌的名称 |
cardType | 卡牌类型,如:伙伴,魔法效果等 |
cost | 卡牌费用 |
content | 卡牌描述 |
attack | 卡牌攻击 |
life | 卡牌生命 |
游戏的背景,就选择编程界,一来是熟悉,二来目前文章前阅读的大家也都能懂,那我们先来设计一张最简单的卡牌吧。最近互联网寒冬,那就设计一个被开除的程序员吧:
{ id: 1, name: "被开除的员工", cardType: 1, cost: 3, content: "", attack: 4, life: 4 }
接下来,就要使用这个数据制作卡牌了,那就留到下一章吧~
再提一次,目前最新进度游戏线上地址 ,欢迎大家注册了之后体验(如果没有人对战,可以单人剧情体验),希望大家多提建议!
下一章或许我会把三章一起开始录制视频(取决于我懒不懒)
2 条评论
zhongwei · 2019年5月12日 22:05
汗,为什么你的github上前端代码跟你的demo最新差这么多
xiejingyang · 2019年5月14日 15:54
不好意思哈,我之前打算是慢慢出教程,一步步的提交代码,用版本号或者branch来对应每次的教程,不过现在太懒好久没更新了。
我会把整个代码直接在github上面公开,发在下一篇文章里。