效果
1、打开两个窗口,输入名称进行登陆
2、开始聊天
思路
做好三件事:
一、写好cocos的界面
二、搭建本地服务器
三、写好cocos的脚本
步骤
一、写好cocos的界面
主要有两个,一个是登陆面板(login节点),一个是聊天面板(chat节点)。
下图是登陆面板。
下图是聊天面板
需要注意一个细节,滑动视图的content要添加垂直布局,不然聊天记录会重叠在一起。
同时还需要将chat--New ScrollView--view--content--item制作成预制。因为后面要不断地产生聊天记录,就是用item来生成的。做成预制之后就可以把item节点给删掉。
这样界面就好了,接下来搭建本地服务器。
二、搭建本地服务器
当你在玩单机游戏的时候,你的电脑会响应你的一切操作,但是如果要玩多人游戏,则需要远程的服务器来同步你和别的玩家的状态。远程服务器是要花钱的,所以我们可以搭建一个简单的本地服务器。这里要用到Node.js,它是一个平台,具体干嘛的我也说不清。先去node官网下载它吧,然后终端检测一下是否安装成功。
node -v
// v12.14.0
接着就开始搭建服务器啦。找一个你喜欢的位置新建文件夹ChatServer,再在这个文件夹下新建文件夹Server,在此目录下运行如下命令
npm install nodejs-websocket
继续在Server目录下新建server.js,写一个每当有客户端连接时就会打印的功能。
// server.js
// 导入模块,使用ws变量接收
var ws = require("nodejs-websocket");
// createServer方法会在每个连接到来时执行一次
// 其中conn参数为到来的连接
var server = ws.createServer(function (conn) {
console.log("一个新的来自客户端的连接!");
}).listen(3000);
我们让这个服务器运行起来,在Server目录下执行命令:
node server
执行了不会有什么反应,但是服务器已经运行起来了。接下来写cocos客户端的脚本,我们会在下面的内容检验客户端能否连上这个正在运行的服务器。
三、写好cocos的脚本
1、连接服务器
在cocos中我们只需要写一个JS脚本,我把它命名为Main,放在scripts目录下,挂到Canvas节点上。在下文中我可能直接把这个脚本说成客户端代码。
学过网络编程的同学都知道,客户端开始运行的时候,要和指定的服务器进行连接,因此start函数写为如下,一旦客户端成功连上服务器端就打印“连接成功”。
// Main.js
cc.Class({
extends: cc.Component,
properties: {
},
start () {
let self = this;
// 进行本地连接,3000 端口
self.ws = new WebSocket("ws://localhost:3000");
self.ws.onopen = function (event) {
console.log("连接成功!");
};
},
});
cocos运行一下,注意这里要确保第二步的服务器正在运行。
可以看到客户端能连上服务器,打印了连接成功。同时服务器也进行了打印。
这样,我们的网络模块就搞通了,接下来要继续完善客户端代码。
2、动态生成item预制体
第一步说了,聊天时需要不断地产生item预制体,这里引用一下官方文档的说法:
在运行时进行节点的创建(
cc.instantiate
)和销毁(node.destroy
)操作是非常耗费性能的,因此我们在比较复杂的场景中,通常只有在场景初始化逻辑(onLoad
)中才会进行节点的创建,在切换场景时才会进行节点的销毁。如果制作有大量敌人或子弹需要反复生成和被消灭的动作类游戏,我们要如何在游戏进行过程中随时创建和销毁节点呢?这里就需要对象池的帮助了。
因此我们的代码onLoad里面需要用到对象池,以降低性能的耗费。所以onLoad函数这么写:
// Main.js
cc.Class({
extends: cc.Component,
properties: {
// item预制体
item: cc.Prefab,
},
onLoad () {
// 初始化对象池,使用 this.pool 变量接收
this.pool = new cc.NodePool();
for (let i = 0; i < 200; i++) {
// 克隆预制体,用 item_prefab 变量接收
let item_prefab = cc.instantiate(this.item);
// 放入对象池中,在需要时使用 get() 方法即可
this.pool.put(item_prefab);
}
},
start () {
let self = this;
// 进行本地连接,3000 端口
self.ws = new WebSocket("ws://localhost:3000");
self.ws.onopen = function (event) {
console.log("连接成功!");
};
},
});
为了测试一下我们能不能动态生成item,在start里面调用新写的createMessage方法。
// Main.js
cc.Class({
extends: cc.Component,
properties: {
// item预制体
item: cc.Prefab,
// 聊天记录的容器
content: cc.Node,
},
onLoad () {
...
},
start () {
// 测试能否动态生成item
this.createMessage('你好');
this.createMessage('我很好');
let self = this;
// 进行本地连接,3000 端口
self.ws = new WebSocket("ws://localhost:3000");
self.ws.onopen = function (event) {
console.log("连接成功!");
};
},
// 创建一个聊天记录,传入参数为字符串
createMessage (str) {
let item_prefab = null;
// 先判断对象池中取没取空
if (this.pool.size() > 0) {
item_prefab = this.pool.get();
} else {
item_prefab = cc.instantiate(this.item);
}
// 为 item_prefab 指定父节点,更改其显示的字符串
item_prefab.parent = this.content;
item_prefab.getComponent(cc.Label).string = str;
},
});
注意把item预制体和content节点拖动关联。
然后运行,就是预期的效果。
3、登陆与聊天的实现
首先是登陆,登陆时用户需要输入名称,这个名称给服务器存着。因此需要给输入框和按钮加上响应事件。输入框事件将name保存起来,按钮事件将name发送给服务器。
// Main.js
cc.Class({
properties: {
...
// 登陆页面
login: cc.Node,
},
...
start () {
...
// 监听服务端发来消息
self.ws.onmessage = function (event) {
console.log(event.data);
// 创建消息
self.createOneLab(event.data);
};
},
// 用户起名框输入完成后
name_onEditDidEnded (editBox) {
// this.name 用于接收你输入的昵称
this.name = editBox.string;
},
// 登入按钮点击
loginClick () {
// 如果为空或者未定义或者为空字符串,返回
if (!this.name || this.name == '') {
return;
}
// 为了与后面的聊天信息区分,我们约定 0为昵称,1为聊天信息
this.ws.send("0:" + this.name);
// 然后隐藏登入页
this.login.active = false;
},
});
把事件挂上去,这里指展示了输入框的,按钮的一样的,省略了。
接下来写服务端代码,服务器收到昵称,发送广播至所有已连接的客户端,告诉他们该用户加入了房间。
// server.js
// 导入模块,使用ws变量接收
var ws = require("nodejs-websocket");
// createServer方法会在每个连接到来时执行一次
// 其中conn参数为到来的连接,listen是监听的端口
var server = ws.createServer(function (conn) {
console.log("一个新的来自客户端的连接!");
// 监听客户端发来的字符串数据
conn.on("text", function (str) {
// 首数字为0时
if (str[0] == '0') {
// 以:分割
var s = str.split(":");
// 打印昵称
console.log("客户端发来昵称:" + s[1]);
// 存储昵称至conn.name
conn.name = s[1];
// 服务器广播消息至所有客户端
broadcast(server,conn.name + "加入房间!");
}
});
}).listen(3000);
// 向目前连接的所有客户端发送消息
function broadcast(server, msg) {
server.connections.forEach(function (conn) {
conn.sendText(msg);
})
}
为了防止服务器报错而停止运行,正在conn.on方法下面再补充两个出现错误和连接断开的方法。
// server.js
...
var server = ws.createServer(function (conn) {
...
// 监听错误出现与连接断开
conn.on("error", function () {
console.log("出现错误!");
});
conn.on("close", function () {
console.log("连接断开!");
});
}).listen(3000);
...
运行服务器,运行客户端,在客户端输入“花花”并发送,
服务器收到“花花”并打印,广播给每个已连接的客户端,
客户端收到服务器的广播,打印“花花加入房间!”。
接下来就是聊天了,和登陆发送呢称是类似的。接下来放出服务器和客户端的完整代码供读者参考。
服务器
// 导入模块,使用ws变量接收
var ws = require("nodejs-websocket");
// 使用createServer方法创建一个连接 .listen(端口号)
// 该方法会在每个连接到来时执行一次
var server = ws.createServer(function (conn) {
// 其中conn参数为到来的连接
console.log("一个新的来自客户端的连接!");
// 监听客户端发来的字符串数据
conn.on("text", function (str) {
// 首数字为0时
if (str[0] == '0') {
// 以:分割
var s = str.split(":");
// 我们打印出来
console.log("客户端发来昵称:" + s[1]);
// 存储昵称至conn.name
conn.name = s[1];
// 服务器广播消息至所有客户端
broadcast(server,conn.name + "加入房间!");
} else if (str[0] == '1') {
var s = str.split(":");
// 我们打印出来
console.log("客户端发来消息:" + s[1]);
// 服务器广播消息至所有客户端
broadcast(server,conn.name + ":" + s[1]);
}
});
// 监听错误出现与连接断开
conn.on("error", function () {
console.log("出现错误!");
});
conn.on("close", function () {
console.log("连接断开!");
});
}).listen(3000);
// 方法: 向目前连接的所有客户端发送消息
function broadcast(server, msg) {
server.connections.forEach(function (conn) {
conn.sendText(msg);
})
}
// 打印,省的控制台空旷
console.log('本地服务端开始运行!');
客户端
// Main.js
cc.Class({
extends: cc.Component,
properties: {
// item预制体
item: cc.Prefab,
// 聊天记录的容器
content: cc.Node,
// 登陆页面
login: cc.Node,
// 文本框
input: cc.Node,
},
onLoad () {
// 初始化对象池,使用 this.pool 变量接收
this.pool = new cc.NodePool();
for (let i = 0; i < 200; i++) {
// 克隆预制体,用 item_prefab 变量接收
let item_prefab = cc.instantiate(this.item);
// 放入对象池中,在需要时使用 get() 方法即可
this.pool.put(item_prefab);
}
},
start () {
let self = this;
// 进行本地连接,3000 端口
self.ws = new WebSocket("ws://localhost:3000");
self.ws.onopen = function (event) {
console.log("连接成功!");
};
// 监听服务端发来消息
self.ws.onmessage = function (event) {
console.log(event.data);
// 创建消息
self.createMessage(event.data);
};
},
// 创建一个聊天记录,传入参数为字符串
createMessage (str) {
let item_prefab = null;
// 先判断对象池中取没取空
if (this.pool.size() > 0) {
item_prefab = this.pool.get();
} else {
item_prefab = cc.instantiate(this.item);
}
// 为 item_prefab 指定父节点,更改其显示的字符串
item_prefab.parent = this.content;
item_prefab.getComponent(cc.Label).string = str;
},
// 用户起名框输入完成后
name_onEditDidEnded (editBox) {
// this.name 用于接收你输入的昵称
this.name = editBox.string;
},
// 登录按钮点击
loginClick () {
// 如果为空或者未定义或者为空字符串,返回
if (!this.name || this.name == '') {
return;
}
// 我们约定 0: 类型为昵称,1: 类型为聊天信息
this.ws.send("0:" + this.name);
// 然后隐藏登入页
this.login.active = false;
},
// 输入聊天信息后
text_onEditDidEnded (editBox) {
// this.text 用于接收你输入的聊天文本
this.text = editBox.string;
},
// 发送按钮点击
sendClick () {
// 如果为空或者未定义或者为空字符串,返回
if (!this.text || this.text == '') {
return;
}
// 我们约定 0: 类型为昵称,1: 类型为聊天信息
this.ws.send("1:" + this.text);
// 然后重置text为空字符
this.text = '';
this.input.getComponent(cc.EditBox).string = '';
},
});
效果就如开头所述。