写在前头
本实例主要给刚接触CocosCreator和网络开发小白使用。老司机可以马上调头,这是开往幼儿园的专列。
背景
聊天室作为大多数网络游戏开发人员的第一个项目,实现了一对多,一对一的数据交互,作为基石一般的存在,可以在上面搭建出各种复杂多变的网络程序,此篇作为我网络游戏开发的第一篇,希望与大家共勉。
服务器的选择
与CocosCretor搭配的全栈解决方案当然是Nodejs了
选型 |
结果 |
Pomelo 比较适合有一定的网络开发基础的人使用。 |
PASS |
纯WebSocket的开发相对比较简单,但是有更好的方案. |
PASS |
Socket.io 反正我没找到更好的… |
Bingo |
服务器的开发
码第一行代码之前默念 善哉善哉 bug去也
1.安装npm包
找到你的项目目录执行以下包安装命令
1 2 3
| npm install express --save npm install socket.io --save npm install underscore --save
|
2.引入包
编写一个app.js的文件
1 2 3 4 5
| const express = require('express'); const app = express(); const http = require('http').Server(app); const sio = require('socket.io')(http); const _ = require('underscore');
|
3.设置跨域访问
之后使用express做工具服务器的时候会用到
1 2 3 4 5 6 7 8 9
| app.all('*', function(req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "X-Requested-With"); res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS"); res.header("X-Powered-By",' 3.2.1') res.header("Content-Type", "application/json;charset=utf-8"); next(); });
|
4.绑定端口
1 2 3 4 5 6 7
| app.get('/',(req,res)=>{ res.send('<h1>Hello world</h1>'); });
http.listen(3000, ()=>{ console.log('listening on http://127.0.0.1:3000'); });
|
5.测试绑定
启动服务器
打开网址查看 出现HelloWorld即为成功
http://127.0.0.1:3000
6.编写一个简单聊天服务器
- 代码量50行左右 有完整的注释
- 简单的分析一下
- sio的on和emit与nodejs的事件监听和触发相似
- connection用来监听客户端的链接
- socket是获得的客户句柄
- socket.on 用于注册自定义事件
- user和msgObj的内容在客户端的Params.ts里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
var userList = [];
sio.on('connection',socket=>{ socket.on('login',(user)=>{ console.log('login'); console.log(user); user.id = socket.id; userList.push(user); sio.emit('userList',userList); socket.emit('userInfo',user); socket.broadcast.emit('loginInfo',user.name+"上线了。"); }); socket.on('disconnect',()=>{ let user = _.findWhere(userList,{id:socket.id}); if(user){ userList = _.without(userList,user); sio.emit('userList',userList); socket.broadcast.emit('loginInfo',user.name+"下线了。"); }
}); socket.on('toAll',function(msgObj){ socket.broadcast.emit('toAll',msgObj); }); socket.on('toOne',function(msgObj){ let toSocket = _.findWhere(sio.sockets.sockets,{id:msgObj.to}); toSocket.emit('toOne', msgObj); });
socket.on('game_ping',function(data){ socket.emit('game_pong'); }); });
|
聊天室基本界面
1 2 3 4 5
| graph TD H(loading界面)-->A A(用户名输入界面)-->B(聊天室界面) B-->D(群聊界面) B-->E(私聊界面)
|
项目的实际界面截图
登录界面
聊天室的界面
发群聊消息
发私聊消息
简单起见 私聊消息红色显示
用户下线通知
用户上线通知
客户端的开发
客户端代码会在后面放出 简单的分析下几个类
NetUtil.ts 参考麒麟子的net.js
- ts中window.io是不存在的需要使用转化获得
1
| const io = (window as any).io || {};
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| init(){ let opts = { 'reconnection':false, 'force new connection': true, 'transports':['websocket', 'polling'] } this.sio = io.connect('http://127.0.0.1:3000',opts);
this.sio.on('connect',(data)=>{ console.log('connect'); this.connected = true; })
this.sio.on('disconnect',(data)=>{ console.log("disconnect"); this.connected = false; });
this.startHearbeat(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
on(event:string,cb){ this.sio.on(event,cb); }
emit(event:string,data?:any){ if(data){ this.sio.emit(event,data); return; } this.sio.emit(event); }
|
LoadingCtrl.ts
- 就干了一件事情初始化连接
- 之后要优于其他操作的初始化都放到这里 比如 i18n
1 2 3 4 5
| initGame(){ NetUtil.Instance.init();
}
|
LoginCtrl.ts
1 2 3 4 5 6 7 8
| login(){ let name = this.userBox.string; if(name.length<2){ return; } let user:User ={id:"",name,imgUrl:""}; NetUtil.Instance.emit('login',user); }
|
ChatCtrl.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| onLoad () { NetUtil.Instance.on('loginInfo',(msg:string)=>{ cc.log(msg); Toast.makeText(msg,Toast.LENGTH_LONG).show(); }) NetUtil.Instance.on('userInfo',(user:User)=>{ GameUtil.Instance.userInfo = user; cc.log(user); }) NetUtil.Instance.on('userList',(userList)=>{ cc.find('Canvas/login_room').active = false; this.userList = userList; this.updateUserList(this.userList); }); NetUtil.Instance.on('toAll',(msg:Message)=>{ let node = cc.instantiate(this.otherMsgItem); node.getChildByName('name').getComponent(cc.Label).string = msg.from.name; node.getChildByName('msgBox').getChildByName('msg').getComponent(cc.Label).string = msg.msg; this.msgContent.addChild(node); if(this.msgContent.height>480){ this.msgScrollView.scrollToBottom(0.3); } }); NetUtil.Instance.on('toOne',(msg:Message)=>{ let node = cc.instantiate(this.otherMsgItem); node.getChildByName('name').getComponent(cc.Label).string = msg.from.name; node.getChildByName('msgBox').getChildByName('msg').getComponent(cc.Label).string = msg.msg; node.getChildByName('msgBox').getChildByName('msg').color = cc.Color.RED; this.msgContent.addChild(node); if(this.msgContent.height>480){ this.msgScrollView.scrollToBottom(0.3); } }); }
|
UserNode.ts
- 预制件通过user初始化
- 注册了点击事件
- 点击展示单独发送的面板
1 2 3 4 5 6 7 8
| init(user:User){ this.user = user; this.node.on(cc.Node.EventType.TOUCH_END,event=>{ let chatCtrl = cc.find('Canvas/chat_room').getComponent(ChatCtrl); chatCtrl.showSingleBox(this.user); }); }
|
Params.ts
1 2 3 4 5 6 7 8 9 10 11
| export interface User{ id:string; name:string; imgUrl:string; }
export interface Message{ from:User ; msg:string ; to?:string ; }
|
加入频道,实现分频道聊天
- 频道的概念即为游戏中分房间的概念,游戏房间中的数据只对房间内的人广播,可以大大提高效率,减少消耗
之前用于保存用户的数组改为了对象
修改后的用户登录接口
- userList[user.channel] 用来保存用户
- join方法用于创建和加入频道
- to方法用于切换到频道
- 下面会出现的leave用于离开频道
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| socket.on('login',(user)=>{ console.log('login'); console.log(user); user.id = socket.id; if(!userList[user.channel]){ userList[user.channel]=[]; } userList[user.channel].push(user); socket.join(user.channel); socket.channel = user.channel; sio.to(user.channel).emit('userList',userList[user.channel]); socket.emit('userInfo',user); socket.broadcast.to(user.channel).emit('loginInfo',user.name+"上线了。"); });
|
客户端处修改
1 2 3 4 5 6 7
| export interface User{ id:string; name:string; channel:string; imgUrl:string; }
|
加入数据库和用户校验
图例
注册一个用户
校验用户名 不能重复
上线提示
数据库内容查询
聊天数据
数据库相关
- 使用sequelize(orm框架)操作数据库
- 可以在不修改代码的情况下更换数据库
- 当前使用sqlite3数据库 也可以切换到mysql
DbClient.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const Sequelize = require('sequelize');
const sequelize = new Sequelize('super', 'root', '123123', { host: '127.0.0.1', port: 3306, dialect: 'sqlite', operatorsAliases: false, pool: { max: 5, min: 0, acquire: 30000, idle: 10000 }, storage:'./db/db.sqlite' });
sequelize.authenticate().then(() => { console.log('数据库连接成功'); }).catch(err => { console.error('数据库连接失败', err); });
module.exports = sequelize;
|
mondels/User.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const Sequelize = require('sequelize'); const dbClient = require('../DbClient');
let User = dbClient.define('tb_users', { uid:{type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true}, account:{type:Sequelize.STRING , allowNull:false}, password:{type:Sequelize.STRING , allowNull:false}, imgurl:{type:Sequelize.STRING , }, channel:{type:Sequelize.STRING , }, age:{type:Sequelize.INTEGER, defaultValue:0}, });
User.sync();
module.exports = User;
|
mondels/Message.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const Sequelize = require('sequelize'); const dbClient = require('../DbClient');
let Message = dbClient.define('tb_messages', { uid:{type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true}, fromuid:{type: Sequelize.INTEGER , allowNull:true}, message:{type:Sequelize.STRING(1024) , allowNull:false}, touid:{type:Sequelize.STRING , defaultValue:""} });
Message.sync();
module.exports = Message;
|
服务器中添加一个注册的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ................
socket.on('register',registerObj=>{ User.findOrCreate({where: {account: registerObj.name}, defaults: {password:registerObj.password}}) .spread((user, created) => { socket.uid = user.get('uid'); if(created){ socket.emit('registerInfo','注册成功'); } else{ socket.emit('registerInfo','用户名已经存在'); } }) }); ..............
|
更新下载:
GitHub地址:https://github.com/SeaPlanet/cocoscreator_chat
Tips:
Socket.io的默认事件列表
服务端事件
事件名称 |
事件解释 |
connection |
socket连接成功之后触发,用于初始化 |
message |
客户端通过socket.send来传送消息时触发此事件 |
anything |
收到任何事件时触发 |
disconnect |
socket失去连接时触发 |
客户端事件
事件名称 |
事件解释 |
connect |
连接成功 |
connecting |
正在连接 |
disconnect |
断开连接 |
connect_failed |
连接失败 |
error |
错误发生,并且无法被其他事件类型所处理 |
message |
同服务器端message事件 |
anything |
同服务器端anything事件 |
reconnect_failed |
重连失败 |
reconnect |
成功重连 |
reconnecting |
正在重连 |