Unity与Node后端-SocketIO通信(一)

前言:

今天开始来跟着SIKI学院学习Nodejs与Unity的交互,其实我以前学过一些nodejs,这篇文章我就将我学的不清晰的地方和以前不会的地方来做下笔记。

 

 

 

 

 

 

 

 

 


NodeJS

本质:不是语言,是谷歌的V8引擎来解释js代码环境

nodejs的特点

优点:

  • 异步IO:非阻塞,在主线程完成后才会处理事件
  • 事件与回调
  • 单线程:避免多线程上下文来回切换的开销

缺点:

  • 无法利用多线程的CPU
  • 出现大量运算,异步操作就不能持续进行下去(不过nodejs支持开启子进程)

 

v8内存信息

像Java一样,nodejs也具有内存管理机制,可以用来自动算法将该销毁的内存销毁。

nodejs是运行在v8上的,v8将nodejs的变量全部都放在堆上。

 

process.memoryUsage() 方法返回 Node.js 进程的内存使用情况的对象,该对象每个属性值的单位为字节。

heapTotal 和 heapUsed 代表 V8 的内存使用情况(总共的和使用了的)。 external 代表 V8 管理的,绑定到 Javascript 的 C++ 对象的内存使用情况。 rss 是驻留集大小(常驻内存), 是给这个进程分配了多少物理内存(占总分配内存的一部分),这些物理内存中包含堆、代码段、以及栈。

对象、字符串、闭包等存于堆内存。 变量存于栈内存,实际的 JavaScript 源代码存于代码段内存。

 

分代式垃圾回收概念

v8采用的主要是分代式垃圾回收机制。

我们来看代码

var test0=null
global.test=function(){

    var test1=null
    console.log("test")
}
test()

代码中的test0变量的存活时间比较长,一直占用着资源,这样的变量就是老生代变量。test1变量的生命周期只有短短的一小点区域,像这样的变量,我们就叫它:“新生代变量

 

应用于新老生代对象的回收算法

可以参考这篇文章,写的详细

 

 

堆外内存

上面的都是在说堆内存,可实际上还存在堆外内存——buffer

因为我们的node语言是处理后端,所以为了安置大量的缓冲流存放到buffer中

js语言本身只有字符串数据类型,没有二进制数据类型,做后端就无可避免的要处理很多TCP流等二进制流,所以我们使用buffer来存放二进制数据的缓冲区,还能保证不会受堆内存的限制。

我们来做个试验

var buffer=new Buffer("我是ndy");
console.log(buffer)

输出:

<Buffer e6 88 91 e6 98 af 6e 64 79>

 

buffer

官网奉上

buffer的构造方法中可以直接指定内存大小(否则可能造成浪费)

//10字节大小的buffer内存
var buffer=new Buffer(10);

buffer通过构造函数的实例来获得操作对象的话,对内存的操作权限是很大的,因为它本身是一个内存缓冲区,所以可能会获取到一些敏感信息,所以我们一般不会用 new 来构造buffer对象,官方提供了这样的形式:

buffer=Buffer.from("我是ndy")

或者

//alloc:内存大小,初始化,编码集
buffer=Buffer.alloc(10,1,"utf8")
console.log(buffer)

alloc的输出:

<Buffer 01 01 01 01 01 01 01 01 01 01>

 

buffer中的值可以被看做是一个数组(即可以通过下标索引),且每一个值的十进制范围是 0-255(256个数),如果你输入一个负数,他会给你加256,直到你回到 0-255 的范围;如果你给了一个大于255的数字,他会减256,直到你回到 0-255 的范围;

buffer[0]=-100
console.log(buffer[0])

输出:

156

buffer分配内存:

buffer类是由c++和js两部分构成

  • c++:负责内存申请部分的操作
  • js:负责分配内存

而在node中,使用了slab分配内存的机制

node当中申请的每一块slab,大小都是8k(一个对象有8k)

所以,所有小于8k的对象是一个小对象,而大于8k的对象则是大对象

小对象(小于8k):

buffer在分配内存的过程当中,会用到pool这样一个局部对象,实现代码大概如下

var pool
function allocPool(){
    pool=new slowBuffer(Buffer.poolSize)
    pool.used=0;
}

其中used是一个内存指针,一开始为零,比如说存进来了一个1024字节的对象,首先会判断一个slab的剩余空间够不够,如果够,就将对象放进内存(反之去找其他slab),那么used就会指向1024,即表示已经使用了1024字节。

这里有一个有趣的现象,比如我第一块slab占用了1k,现在又来了一个8k的对象,那么他就会存放于第二块slab中(因为第一块只剩7k了),如果再来一个对象,它只会和第二块slab进行判断,以此类推。第一块slab的剩余7k从此浪费了。

另外注意,一个slab中,只要存在还在使用的对象,那这个slab就不可以释放

对了,还有:每一个buffer对象有一个parent属性,这个属性指向的其实就是slowBuffer,而这个slowBuffer的对象实际上是在c++中定义的(这就不是v8引擎分配的了,是c++底层分配)

 

大对象(大于8k):

如果出现大对象,会单独申请一块匹配的对象进行保存,而不会使用共有的slab。

 

利用buffer中文字符串拼接:

中文字符串的拼接经常会出现问题,我们来做个试验

首先创建一个txt文件,编码要UTF-8格式

txt文件改成UTF-8格式:

另存为,覆盖原文件,选择好编码方式

#Test.txt
你说啥,我看不见

我们先利用流来读取文件内容

var fs=require('fs')

var re=fs.createReadStream("./Test.txt")
var text=""
re.on("data",function(chunk){
    text+=chunk
})
re.on("end",function(){
    console.log(text)
})

输出:

你说啥,我看不见

没有问题

可是若我们限定了每次流读取的大小,那么就会出现问题(一个汉字三个字节,假设我们限定每次读取七个字节,第三个汉字就会有问题,造成歧义)

var fs=require('fs')

var re=fs.createReadStream("./Test.txt",{highWaterMark:7})
var text=""
re.on("data",function(chunk){
    text+=chunk
})
re.on("end",function(){
    console.log(text)
})

输出:

你���啥��我看不见

第一种解决方式很简单,直接规定好读入的编码格式,告诉它读取规则

var fs=require('fs')

var re=fs.createReadStream("./Test.txt",{highWaterMark:7})
re.setEncoding("utf8")
var text=""
re.on("data",function(chunk){
    text+=chunk
})
re.on("end",function(){
    console.log(text)
})

即恢复正常

但是这样只能解决部分格式,如果出现一些刻薄的格式,这个方法也会乱码。

我们出现的问题是因为限定了读入的大小长度所造成的说的,那么,将读入的东西先拼接起来就好了

所以,最好的方法就是第二种方法——拼接

var fs=require('fs')

var re=fs.createReadStream("./Test.txt",{highWaterMark:7})

var texts=[]
var size=0

re.on("data",function(chunk){
    texts.push(chunk)
    size+=chunk.length
})
re.on("end",function(){
    //concat:将数组在size大小内进行拼接
    var text=Buffer.concat(texts,size)
    console.log(text.toString())
})

这样就可以根本上解决问题

 

网络编程

服务器

我的那个nodejs的基本学习中聊过服务器这块,大家可以去先看看那个

建立TCP服务端

字段 含义
URG 紧急指针是否有效。为1,表示某一位需要被优先处理
ACK 确认号是否有效,一般置为1。
PSH 提示接收端应用程序立即从TCP缓冲区把数据读走。
RST 对方要求重新建立连接,复位。
SYN 请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1
FIN 希望断开连接。

TCP的三次握手是怎么进行的了:发送端发送一个SYN=1,ACK=0标志的数据包给接收端,请求进行连接,这是第一次握手;接收端收到请求并且允许连接的话,就会发送一个SYN=1,ACK=1标志的数据包给发送端,告诉它,可以通讯了,并且让发送端发送一个确认数据包,这是第二次握手;最后,发送端发送一个SYN=0,ACK=1的数据包给接收端,告诉它连接已被确认,这就是第三次握手。之后,一个TCP连接建立,开始通讯。

而四次挥手,关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

跟多详情请看这里

建立TCP服务端很简单:

var net=require("net")
//当有客户端连接到服务器上,则会执行该回调
var server=net.createServer(function (socket){
    socket.on("data",function(){
        
    })
})
//绑定端口等信息
server.listen(1100,"127.0.0.1")

核心代码就是这样

 

建立UDP服务端

UDP(用户数据包协议)不同于面对连接的TCP协议(因为面对连接,所以需要三次握手建立连接),UDP协议实际更像是一个广播,只管发包,不管有没有收到,所以速度较快,但是发包丢失率也会增加。

建立UDP核心代码:

var dgram=require("dgram")

var server=dgram.createSocket("udp4")
server.on("message",function(msg,rinfo){
    console.log("msg:"+msg+"  "+"rinfo:"+rinfo)
})
//创建一个新的udp连接的时候,会触发listening
server.on("listening",function(){
    console.log("Start UDP server")
})
server.bind(1100)

我们再来写个客户端

var dgram=require("dgram")

var msg=Buffer.from("你好。")
var client=dgram.createSocket("udp4")
client.send(msg,1100,"127.0.0.1",function(err,data){
    client.close()
})

好了,先启动服务端然后启动客户端,然后就可以看到服务端输出了“你好。”

通信成功。

服务器模型的演变过程

  • 同步处理模型:一开始的服务器是同步处理数据,但是这样明显很不合理,每来一个请求就要排队等前面的请求处理完。
  • 复制进程模型:所以后面有了复制进程的方法,每来一个请求就复制一个进程(子进程),但是这样的开销也很可怕
  • 多线程处理模型:现在主流的就是多线程处理方式,这样开销会小很多,每一个线程都有自己的堆栈,每一个线程根据自己资源的不同占用一定量的内存空间,但在并发特别高的时候,多线程的内存占用也是非常高。
  • 事件驱动服务模型:比如我们的node,它是单线程的,它避免了不必要的内存消耗以及线程上下切换造成的开销。只要CPU允许,他就可以事件驱动。

 

子进程Spawn方法

NodeJS的JavaScript运行在单个进程的单个线程上,一个JavaScript执行进程只能利用一个CPU核心,而如今大多数CPU均为多核CPU,为了充分利用CPU资源,Node提供了child_process和cluster模块来实现多进程以及进程管理。本文将根据Master-Worker模式,搭建一个简单的服务器集群来充分利用多核CPU资源,探索进程间通信、负载均衡、进程重启等知识。

示例代码:

var chirdProccess=require("child_process")
//创造子进程,执行带参命令,保存进程对象
var worker=chirdProccess.spawn("node",["test2.js"])
worker.stdout.on("data",function(data){
    console.log("子进程打印了"+data)
})

其中var worker=chirdProccess.spawn("node",["test2.js"])就是一个子进程,参数是一个cmd命令——“node test2.js”,它会执行另一个文件test2.js

#test2.js
console.log("子进程")

worker.stdout.on("data",function(data){
console.log("子进程打印了"+data)
})
这一句即得到子进程的标准输出,并打印在现在这个控制台中

最后输出结果:

子进程打印了子进程

子进程Exec方法

上面的方法还有一种更简单的写法就是利用exec函数

var chirdProccess=require("child_process")
var worker
worker=chirdProccess.exec("node test2",function(err,stdout,stderr){
    console.log(stdout)
})

这样可以用自带的回调函数直接输出结果,不必再绑定事件

子进程ExecFile方法

execFile方法:

var chirdProccess=require("child_process")

var worker=chirdProccess.execFile("node",["test2.js"],function(err,stdout,stdin){
    console.log(stdout)
})

第一个参数是可执行文件的名称或者路径,第二个参数是给可执行文件传的参数,最后是一个回调函数

其实spawn和exec是将node作为命令执行了,node本质就是一个可执行程序。

子进程Fork方法

child_process.fork() 方法是 child_process.spawn() 的一个特例,专门用于衍生新的 Node.js 进程。 与 child_process.spawn() 一样返回 ChildProcess 对象。 返回的 ChildProcess 将会内置一个额外的通信通道(IPC通信通道),允许消息在父进程和子进程之间来回传递。

var chirdProccess=require("child_process")

var worker=chirdProccess.fork("./test2.js",{
    execArgv:['--inspect='+(process.debugPort+1)]
})

fork指定一个文件,node启动;但是要注意要配置好端口,否则端口会显示被占用(上例将端口号加了1)

fork不仅会创建子进程,还会创建一个IPC通信通道,这样父子进程就可以通信了

父进程:

var chirdProccess=require("child_process")

var worker=chirdProccess.fork("./test2.js",{
    silent:true,
    execArgv:['--inspect='+(process.debugPort+1)]
})
worker.stdout.on("data",function(data){
    console.log(data.toString())
})
worker.send('来自于父进程的消息')
worker.on("message",function(data){
    console.log(data)
})

子进程

console.log("子进程")

process.on("message",function(data){
    console.log(data)
})
process.send("来自子进程的消息")

 

 


JS的基础知识

基础数据类型:

  1. JavaScript(以下简称js)的数据类型分为两种:原始类型(即基本数据类型)和对象类型(即引用数据类型);
  2. js常用的基本数据类型包括undefined、null、number、boolean、string;
  3. js的引用数据类型也就是对象类型Object,比如:Object、array、function、data等;

相等运算符:

  • ==:相等
  • ===:严格相等
console.log(null==undefined)
console.log(null===undefined)
console.log(123=='123')
console.log(123==='123')

true
false
true
false

 

数组:

//数组
var array=[]
//从后面添加元素
array.push("aa")
//通过int下标添加元素
array[1]="bb"
//从前面添加元素
array.unshift("cc")
//插入元素,删除第零个元素
array.splice(1,0,"ddd")

//键值对
var array2=[]
array2['r']='sdf'

console.log(array)
console.log(array2)

[ ‘cc’, ‘ddd’, ‘aa’, ‘bb’ ]
[ r: ‘sdf’ ]

 

对象:

js中,对象的概念是最基本最重要的概念,几乎可以将所有的东西全都看成对象

js中没有类

通过构造函数创建

var map=new Object()
map["aa"]="ss"
console.log(map)
console.log(map.aa)

{ aa: ‘ss’ }
ss

 

通过字面量创建对象:

map={
    aa:'abc',
    bb:'tyu'
}
console.log(map)

{ aa: ‘abc’, bb: ‘tyu’ }

 

函数:

动态语言直接写形参即可

function Test(num){
    console.log(num)
}
Test(345)

 

循环:

for(var i=0;i<10;i++){
    console.log("123")
}
var array=["aa","bb","cc","dd"]
for (var i in array){
    //注意,i不是array中的元素,而是下标
    console.log(array[i])
}
array.forEach(Element=>{
    console.log(Element)
})

 

类型判断与类型转换:

var map={
    aa:1,
    bb:2
}
console.log(typeof map)
var test
console.log(typeof test)
//“空”是一个对象 
var test2=null
console.log(typeof test2)

console.log(typeof (String(test2)))

console.log(typeof (Number(test2)))

 

常见问题:

在js中,允许先使用后定义:

console.log(i)
var i

输出:
undefined

js会将你的声明提升到最上面,但是也仅仅是提升变量的声明,你获取的变量会是一个默认值——undefined

console.log(i)
var i=1

输出:
undefined

关键字定义

重复声明

  • var:可以重复声明
var i=0
var i=2
console.log(i)
  • let:不可重复声明

作用域

  • var:无视“块”的分隔
var a=10
{
    var a=2
}
console.log(a)

输出:2

 

  • let:受“块”的影响
let a=10
{
    let a=2
}
console.log(a)

输出:10

 


案例笔记

基本通信:

客户端使用Unity Asset可以下载的插件 socted io来进行socket通信,进行简单测试:

服务端:

const io=require("socket.io")(3000)

//添加连接事件
io.on("connection",function(socket){
    console.log("已经连接!")
})

客户端:

一个空物体,挂载脚本,内容如下:

using SocketIO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NetWorkMgr : MonoBehaviour
{
    private SocketIOComponent _socket;
    // Start is called before the first frame update
    void Start()
    {
        _socket = gameObject.AddComponent<SocketIOComponent>();
        _socket.url = "ws://127.0.0.1:3000/socket.io/?EIO=4&transport=websocket";
        _socket.On(ev:"connection", callback:(data) =>
        {
            Debug.Log(data);
        });
    }
}

先启动服务端,然后启动Unity场景,服务端就会打印消息了

服务端给客户端发送消息

服务端:

const io=require("socket.io")(3000)

console.log("Start server")
//添加连接事件
io.on("connection",function(socket){
    console.log("已经连接!")
    //给客户端发送消息——connection
    socket.emit("connection")
})

客户端:

    void Start()
    {
        _socket = gameObject.AddComponent<SocketIOComponent>();
        _socket.url = "ws://127.0.0.1:3000/socket.io/?EIO=4&transport=websocket";
        _socket.On(ev: "connection", callback: (data) =>
           {
               Debug.Log(message: "收到名为connection的消息");
           });
    }

会打印出“收到名为connection的消息”

客户端给服务端发送消息

服务端:

const io=require("socket.io")(3000)

console.log("Start server")
//添加连接事件
io.on("connection",function(socket){
    console.log("已经连接!")
    //给客户端发送消息——connection
    socket.emit("connection")

    socket.on("Message",function(){
        console.log("GetMessage")
    })
})

客户端:

    void Start()
    {
        _socket = gameObject.AddComponent<SocketIOComponent>();
        _socket.url = "ws://127.0.0.1:3000/socket.io/?EIO=4&transport=websocket";
        _socket.On(ev: "connection", callback: (data) =>
           {
               Debug.Log(message: "收到名为connection的消息");
           });
        emitEven.onClick.AddListener(this.emitEvent);
    }
    private void emitEvent() {
        print("SendMessage");
        _socket.Emit("Message");
    }

我这里绑定了一个按钮,一点按钮,服务端就会输出“GetMessage”

 

事件监听接口:

客户端的基本事件监听接口模型:

public class NetWorkMgr : MonoBehaviour
{
    private SocketIOComponent _socket;


    //私有变量以_开头
    //处理事务逻辑的接口
    private Dictionary<string, NetworkEvent> _eventsDic;
    void Start()
    {
        _eventsDic = new Dictionary<string, NetworkEvent>();
        _socket = gameObject.AddComponent<SocketIOComponent>();
        _socket.url = "ws://127.0.0.1:3000/socket.io/?EIO=4&transport=websocket";
    }
    //添加监听的事件接口
    public void AddListener(string id,Action<SocketIOEvent> callback) {
        //字典中没有这个id,则可以添加
        if (!_eventsDic.ContainsKey(id)) {
            _eventsDic.Add(id, new NetworkEvent(id, _socket));
            //NetworkEvent类的对象就是对于某一个ID,它的事件的载体
        }
        _eventsDic[id].AddLisener(callback);
    }
    //一个ID可能会绑定多个回调函数,所以要指定清楚id和回调函数
    public void RemoveListener(string id, Action<SocketIOEvent> callback) {
        if (_eventsDic.ContainsKey(id)) {
            _eventsDic[id].RemoveLisener(callback);
        }
    }
    //发送消息
    public void Emit(string id,JSONObject json=null) {
        _socket.Emit(id, json);
    }
}

我们将每一个消息体作为一个NetworkEvent类的对象,NetworkEvent类如下:

//此类用来作为NetWorkMgr的字典的值,用来处理一些事件逻辑
//用来处理网络部分事件的绑定还有移除
public class NetworkEvent
{
    public string ID { get; private set; }
    private Action<SocketIOEvent> _action;
    public NetworkEvent(string id,SocketIOComponent socket) {
        ID = id;
        //构造这个对象就会开启这个名为id的事件的监听
        socket.On(id, Excute);
    }

    public void AddLisener(Action<SocketIOEvent> callback) {
        _action += callback;
    }
    public void RemoveLisener(Action<SocketIOEvent> callback)
    {
        _action -= callback;
        
    }
    private void Excute(SocketIOEvent data) {
        if (_action != null) {
            _action(data);
        }
    }
    public void Clear() {
       _action = null;
    }
}

我们每给NetWorkMgr 类的字典里添加一个ID就会生成一个NetworkEvent类的对象,同时开启与ID同名的socket事件的监听,一旦事件被触发,就会调用ID里添加的方法。

 

为了让NetWorkMgr成为一个不随场景消失的东西,我们将NetWorkMgr做成一个单例。

public class NetWorkMgr : MonoBehaviour
{
    //将这个类做成单例模式
    public static NetWorkMgr Instance { get; private set; }

    private SocketIOComponent _socket;

    //私有变量以_开头
    //处理事务逻辑的接口
    private Dictionary<string, NetworkEvent> _eventsDic;
    void Awake()
    {
        InitInstance();
        _eventsDic = new Dictionary<string, NetworkEvent>();
        _socket = gameObject.AddComponent<SocketIOComponent>();
        _socket.url = "ws://127.0.0.1:3000/socket.io/?EIO=4&transport=websocket";
        _socket.On(ev: "connection", callback: (data) =>
           {
               Debug.Log(message: "收到名为connection的消息");
           });
    }
    private void InitInstance() {
        //如果Instance变成了空(调换了场景)
        //则初始化Instance,并不摧毁当前游戏物体
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else {
            //不管NetWorkMrg的物体有多少个,静态的Instance只有一个
            //进入else说明当前已经有了一个Instance
            //所以摧毁我们这个后来者,避免两个单例物体
            Destroy(gameObject);
        }
    }
    //添加监听的事件接口
    public void AddListener(string id,Action<SocketIOEvent> callback) {
        //字典中没有这个id,则可以添加
        if (!_eventsDic.ContainsKey(id)) {
            _eventsDic.Add(id, new NetworkEvent(id, _socket));
            //NetworkEvent类的对象就是对于某一个ID,它的事件的载体
        }
        //调用这个id对应的NetworkEvent对象的AddLisener
        _eventsDic[id].AddLisener(callback);
    }
    //一个ID可能会绑定多个回调函数,所以要指定清楚id和回调函数
    public void RemoveListener(string id, Action<SocketIOEvent> callback) {
        if (_eventsDic.ContainsKey(id)) {
            _eventsDic[id].RemoveLisener(callback);
        }
    }
    //发送消息
    public void Emit(string id,JSONObject json=null) {
        _socket.Emit(id, json);
    }
}

 

建立公共类

public class Keys 
{
    public readonly static string Connection = "connection";
    public readonly static string DisConnection = "disconnection";
}

方便我们后面直接使用变量名进行处理

 

 

视图处理

为相应的功能做相应的视图处理类:

比如连接事件

public class ConnectView : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        NetWorkMgr.Instance.AddListener(Keys.Connection, Connect);
        NetWorkMgr.Instance.AddListener(Keys.DisConnection, DisConnect);
    }
    private void OnDestroy()
    {
        NetWorkMgr.Instance.AddListener(Keys.Connection, Connect);
        NetWorkMgr.Instance.AddListener(Keys.DisConnection, DisConnect);
    }
    //建立连接的方法
    private void Connect(SocketIOEvent data)
    {
        Debug.Log("connect server Success");
    }
    //断开链接的方法
    private void DisConnect(SocketIOEvent data) {
        Debug.Log("Disconnect server Success");
    }
}

我们可以对这类视图事件做一个基类

public abstract class ViewBase : MonoBehaviour
{
    //激活物件会调用
    protected virtual void OnEnable() {
        AddEventLintener();
    }
    //关闭物件会调用
    protected virtual void OnDisable() {
        RemoveEventLintener();
    }
    protected abstract void AddEventLintener();
    protected abstract void RemoveEventLintener();
}

让后面的视图处理类都继承自这个基类,比如一个登陆视图类:

public class LoginView : ViewBase
{
    protected override void AddEventLintener()
    {
        NetWorkMgr.Instance.AddListener(Keys.Login, LoginResult);
    }
    protected override void RemoveEventLintener()
    {
        NetWorkMgr.Instance.RemoveListener(Keys.Login, LoginResult);
    }
    private void LoginResult(SocketIOEvent data) {

    }
}

这样就很方便处理了

将登陆视图与Unity场景的界面的东西绑定:

public class LoginView : ViewBase
{
    private void Start()
    {
        string user = "";
        string passwd = "";
        //给user输入框注册事件监听器
        transform.Find("LoginView/user").GetComponent<InputField>()
            .onEndEdit.AddListener((text)=> {
                user = text;
            });
        //给passwd输入框注册事件监听器
        transform.Find("LoginView/passwd").GetComponent<InputField>()
            .onEndEdit.AddListener((text) => {
             passwd = text;
            });
        //给提交按钮注册发送信息效果
        transform.Find("LoginView/Button").GetComponent<Button>()
            .onClick.AddListener(() => {
                JSONObject json = new JSONObject(JSONObject.Type.OBJECT);
                json.AddField("loginUser",user);
                json.AddField("loginPasswd", passwd);
                NetWorkMgr.Instance.Emit(Keys.Login, json);
            });
    }
    protected override void AddEventLintener()
    {
        NetWorkMgr.Instance.AddListener(Keys.Login, LoginResult);
    }
    protected override void RemoveEventLintener()
    {
        NetWorkMgr.Instance.RemoveListener(Keys.Login, LoginResult);
    }
    private void LoginResult(SocketIOEvent data) {

    }
}

 

登陆通信:

node

#index.js
const io=require("socket.io")(3000)
const login=require('./login')
require("./keys")
console.log("Start server")
//添加连接事件
io.on(keys.Connection,function(socket){
    console.log("已经连接!")
    //开启登录的监听
    login.initLogin(socket)
    //给客户端发送消息——connection
    socket.emit("connection")
})




#login.js
var testAccount=[{user:"111",passwd:"111"},
                {user:"222",passwd:"222"}]
module.exports.initLogin=function(socket){
    socket.on(keys.Login,(data)=>{
        var result={}
        result.user=data.user
        result.result=CheckAccount(data)
        socket.emit(keys.Login,result)

        if(result.result==true){
            //登录成功,开始处理游戏代码
        }
    })
}
function CheckAccount(data){
    var result=false;
    testAccount.forEach(function(Item){
        if(Item.user==data.user&&Item.passwd==data.passwd){
            result=true
        }
    })
    return result
}

C#主要的:

#LoginView.cs
public class LoginView : ViewBase
{
    private void Start()
    {
        string user = "";
        string passwd = "";
        //给user输入框注册事件监听器
        transform.Find("LoginView/user").GetComponent<InputField>()
            .onEndEdit.AddListener((text)=> {
                user = text;
            });
        //给passwd输入框注册事件监听器
        transform.Find("LoginView/passwd").GetComponent<InputField>()
            .onEndEdit.AddListener((text) => {
             passwd = text;
            });
        //给提交按钮注册发送信息效果
        transform.Find("LoginView/Button").GetComponent<Button>()
            .onClick.AddListener(() => {
                JSONObject json = new JSONObject(JSONObject.Type.OBJECT);
                json.AddField("user",user);
                json.AddField("passwd", passwd);
                //像node端发送数据
                NetWorkMgr.Instance.Emit(Keys.Login, json);
            });
    }
    protected override void AddEventLintener()
    {
        NetWorkMgr.Instance.AddListener(Keys.Login, LoginResult);
    }
    protected override void RemoveEventLintener()
    {
        NetWorkMgr.Instance.RemoveListener(Keys.Login, LoginResult);
    }
    private void LoginResult(SocketIOEvent data) {
        //如果该方法被调用,说明node端向C#端发送了key为login的信息
        //data是一个SocketIOEvent对象的json属性,通过下标得到值
        Debug.Log(data.data["result"]);
    }
}
#launchGame.cs(这个脚本挂载在Unity的登录Canvas上)
public class launchGame : MonoBehaviour
{   
    void Start()
    {
        //网络管理组件的初始化
        InitNetworkMgr();
        InitLoginView();
    }
    private void InitNetworkMgr() {
        GameObject mgr = new GameObject("NetWorkMgr");
        mgr.AddComponent<NetWorkMgr>();
        mgr.AddComponent<ConnectView>();
    }
    private void InitLoginView() {
        //添加组件,注册登录消息
        transform.gameObject.AddComponent<LoginView>();
    }
}

开启服务端,再开启客户端,输入登录信息点击登录,输入testAccount中的用户的话,客户端就会收到:“true”,反之false;

注意,客户端封装json时用的键名一定要和服务端取json元素用的键名对应,否则会取不出值

完善登录通信(生成人物)

我们在客户端定义一个新的类——Util,作为用到的工具类

public class Util 
{
    public static bool GetBoolFromJson(JSONObject json) {
        if (json.ToString() == "true")
        {
            return true;
        }
        else if (json.ToString() == "false")
        {
            return false;
        }
        else {
            //记录一下:传入数据不是一个布尔型
            Debug.LogError("the json is not a bool");
            return false;
        }
    }
}

然后在 LoginView 中的 登录响应方法 中这样修改

    private void LoginResult(SocketIOEvent data) {
        //如果该方法被调用,说明node端向C#端发送了key为login的信息
        //data是一个SocketIOEvent对象的json属性,通过下标得到值
        if (Util.GetBoolFromJson(data.data["result"]))
        {
            Debug.Log("Login success");
        }
        else {
            Debug.Log("Login lose");
        }
    }

这样我们就可以看到正确的响应登录的结果了

这里只是验证一下标识,接下来我们来真正做出效果

登录成功让它跳转场景

    private void LoginResult(SocketIOEvent data) {
        //如果该方法被调用,说明node端向C#端发送了key为login的信息
        //data是一个SocketIOEvent对象的json属性,通过下标得到值
        if (Util.GetBoolFromJson(data.data["result"]))
        {
            //同步加载
            SceneManager.LoadScene("Game");
        }
        else {
            Debug.Log("Login lose");
        }
    }

记得要将Game场景加入Build setting中

测试一下没有问题成功跳转,下面我们来丰富Game场景的初始化。

在Game场景中新建一个Canvas,挂载脚本 StartGameView.cs。

脚本中写上如下内容:

public class StartGameView : ViewBase
{

    private void Start()
    {
        //发送消息告诉服务端初始化完成
        NetWorkMgr.Instance.Emit(Keys.InitGameComplete);
    }
    protected override void AddEventLintener()
    {
        //注册事件监听
        NetWorkMgr.Instance.AddListener(Keys.Spawn, spawnPlayer);
    }

    protected override void RemoveEventLintener()
    {
        NetWorkMgr.Instance.RemoveListener(Keys.Spawn, spawnPlayer);
    }
    private void spawnPlayer(SocketIOEvent data) {

    }
}

上面就是StartGameView的基本内容

为了实现多人在线通信,不仅要给新登录的对象传递已经登录对象的数据信息,还要给所有已经登录的对象传递新登录的对象的信息

服务端:

#login.js
const startGame=require('./startGame')
var testAccount=[{user:"111",passwd:"111"},
                {user:"222",passwd:"222"}]

module.exports.initLogin=function(socket){
    socket.on(keys.Login,(data)=>{
        var result={}
        result.user=data.user
        result.result=CheckAccount(data)
        socket.emit(keys.Login,result)

        if(result.result==true){
            //登录成功,开始处理游戏代码
            //调用startGame初始化游戏服务端
            startGame.InintStartGame(socket,result.user)
        }
    })
}
function CheckAccount(data){
    var result=false;
    testAccount.forEach(function(Item){
        if(Item.user==data.user&&Item.passwd==data.passwd){
            result=true
        }
    })
    return result
}



#startGame.js
let players={}
module.exports.InintStartGame=function(socket,playID){
    socket.on(keys.InitGameComplete,function(data){
        //获取当前的player对象
        let player={
            id:playID,
            position:{x:0,y:0,z:0},
            rotation:{x:0,y:0,z:0}
        }
        //将player对象保存到数组当中
        players[playID]=player
        //告诉老对象当前登录对象的信息
        socket.broadcast.emit(keys.Spawn,player)
        //告诉当前登录对象,已在线对象的信息
        for(let player in players){
            //不再发送自己了,避免重复
            if(player.id!=playID)
                socket.emit(keys.Spawn,players[player])
        }
    })
}

客户端:

我们再创建一个脚本PlayerSpawner(角色生成)用来处理所有的人物登录的情况

#PlayerSpawner.cs
public class PlayerSpawner 
{

    //这个类通过根据网路数据来生成用户
    private static PlayerSpawner _instance;
    public static PlayerSpawner Instance {
        get {
            if (_instance == null) {
                _instance = new PlayerSpawner();
            }
            return _instance;
        }
    }
    //字典用来保存已经登录的人物
    private Dictionary<string, GameObject> _playerDic = new Dictionary<string, GameObject>();
    public GameObject SpawnPlayer(string id)
    {
        GameObject player=GameObject.Instantiate(Resources.Load<GameObject>(Paths.Player));
        //将这个角色加入字典
        _playerDic.Add(id, player);
        return player;
    }
    public void RemovePlayer(string id) {
        //销毁物体
        var player = _playerDic[id];
        GameObject.Destroy(player);
        //从字典中移除
        _playerDic.Remove(id);
    }
    public GameObject GetPlayer(string id) {
        return _playerDic[id];
    }
}


#StartGameView.cs
public class StartGameView : ViewBase
{

    private void Start()
    {
        //发送消息告诉服务端初始化完成
        NetWorkMgr.Instance.Emit(Keys.InitGameComplete);
    }
    protected override void AddEventLintener()
    {
        //开启Spawn(生成)player的事件的监听
        NetWorkMgr.Instance.AddListener(Keys.Spawn, spawnPlayer);
    }

    protected override void RemoveEventLintener()
    {
        NetWorkMgr.Instance.RemoveListener(Keys.Spawn, spawnPlayer);
    }
    private void spawnPlayer(SocketIOEvent data) {
        //新建角色
        var player = PlayerSpawner.Instance.SpawnPlayer(data.data["id"].ToString());
    }
}

Resources.Load<GameObject>(Paths.Player)这一句意思是在Resources目录下的Paths.Player(我在别的文件中定义了,定义的是Prefabs/Player)路径物体加载过来。一定要注意要有Resources目录!

我们先不考虑更多信息,先简单将人物可以正常生成

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

发表评论