又废话(前言)

别看这个游戏现在这个垃圾样,我可是摸索了将近半年才写出来的,所以我现在理解一个好游戏要是想做出来,为啥要两三年了。

目前最新进度游戏线上地址 ,欢迎大家注册了之后体验(如果没有人对战,可以单人剧情体验),希望大家多提建议!

第二回合(对战通信)

采购完项目的材料,要开始建地基了。

如果把我们的游戏中的概念日常化,那么对战其实就是两个人进入了一个聊天室(匹配),用我指定的语言(卡牌)在聊天(对战)。所以我们先按照聊天室来编写我们的程序。

首先先给两个人开个房,emmmm……正规房。

作为一个在线对战游戏,需要保证两个人能连到一个房间,并且不掉线,掉线的时候自动重连,听起来这一步就很费功夫了,好在这些需求socket.io都已经实现了,我们就聚焦在怎么实现游戏系统上就ok。进入房间下一步就开始进行广播,当我发送一条消息的时候,由服务器接收这个消息,然后进行处理后再广播给包括我在内的房间里的用户。

这个地方要解释一下为什么要广播给自己,而不只广播给别人在本地处理自己。这个地方我是这么考虑的:
1. 能够统一的处理所有人的同类消息。
2. 游戏是时序重要的,也就是说,每张牌一定要按照打出的顺序处理,不然会有截然不同的结果。
3. 不能在客户端本地做操作,在服务器上做统一的处理以防止本地作弊。

定一个初步的目标是两个人能够进入房间并且同步增加一个计数器。

客户端的连接可以直接使用之前的代码,先做服务端的处理,将恰好两人放入同一个房间。

这里我使用的方法是先使用一个数组保存待匹配的用户,一个缓存保存每个用户当前的房间号码,再使用一个缓存保存每个房间里的游戏数据。在app.js中插入下列代码:

const waitPairQueue = []; // 等待排序的队列
const memoryData = {}; // 缓存的房间游戏数据,key => 房间号,value => 游戏数据
const existUserGameRoomMap = {}; // 缓存用户的房间号, key => 用户标识,value => 房间号

这样,当某个用户加入进来的时候,我们进行下列操作:

  1. 对用户先发送连接成功请等待的命令。
  2. 查看是否有等待匹配的用户,没有则将当前用户加入队列,如果有则取出队列中的一个用户。
  3. 生成一个房间号码,将用户的信息缓存起来,将通过房间号初始化游戏数据。
  4. 用户连入同一个socket服务,并且缓存用户的socket实例。
  5. 告诉用户连接完成,同时发送初始化游戏数据。

在原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
}

接下来,就要使用这个数据制作卡牌了,那就留到下一章吧~

再提一次,目前最新进度游戏线上地址 ,欢迎大家注册了之后体验(如果没有人对战,可以单人剧情体验),希望大家多提建议!

下一章或许我会把三章一起开始录制视频(取决于我懒不懒)


2 条评论

zhongwei · 2019年5月12日 22:05

汗,为什么你的github上前端代码跟你的demo最新差这么多

    xiejingyang · 2019年5月14日 15:54

    不好意思哈,我之前打算是慢慢出教程,一步步的提交代码,用版本号或者branch来对应每次的教程,不过现在太懒好久没更新了。
    我会把整个代码直接在github上面公开,发在下一篇文章里。

回复 xiejingyang 取消回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注