承接前面的内容,继续学习Unity与nodejs通信
上一次问我们实现了成功登陆游戏并且实例化一个游戏对象
现在,我们开始发送聊天内容
新建ChatView.cs脚本,然后在脚本中给发送消息和显示消息两个UI挂载对应的脚本类名
public class ChatView : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
transform.Find("ShowMessage").gameObject.AddComponent();
transform.Find("SendMessage").gameObject.AddComponent();
}
}
我们先来完成SendMessage.cs的内容
public class SendMessage : MonoBehaviour
{
void Start()
{
string message = "";
//发送消息的输入框
InputField input = transform.Find("InputField").GetComponent();
input.onEndEdit.AddListener((text) =>
{
message = text;
});
input.text = "";
//发送消息的按钮
transform.Find("Send").GetComponent
我们封装json中会加入它的user,但是现在先没有写,用0代替
客户端先写成这样,我们回服务端
#StartGame.js
var chatRoom=require('./Chatroom')
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}
}
chatRoom.InitChatRoom(socket)
//将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])
}
})
}
#Chatroom.js
module.exports.InitChatRoom=function(socket){
socket.on(keys.Chat,(data)=>{
//给自己发送这个消息
socket.emit(keys.ReceiveChat,data)
//给其余人发送这个消息
socket.broadcast.emit(keys.ReceiveChat,data)
})
}
然后我们在回头客户端,处理keys.ReceiveChat消息
public class ShowMessage : ViewBase
{
private Transform _content;
private void Start()
{
_content = transform.Find("content/back");
}
protected override void AddEventLintener()
{
NetWorkMgr.Instance.AddListener(Keys.ReceiveChat, showChat);
}
protected override void RemoveEventLintener()
{
NetWorkMgr.Instance.RemoveListener(Keys.ReceiveChat, showChat);
}
private void showChat(SocketIOEvent data) {
string id = data.data["id"].ToString();
string message = data.data["message"].ToString();
SpawnItem().Init(id, message);
}
private ChatItem SpawnItem()
{
GameObject prefab = Resources.Load(Paths.ChatItem);
GameObject item = Instantiate(prefab, _content);
return item.AddComponent();
}
}
#ShowMessage.cs
public class ShowMessage : ViewBase
{
private Transform _content;
private void Start()
{
_content = transform.Find("content");
}
protected override void AddEventLintener()
{
NetWorkMgr.Instance.AddListener(Keys.ReceiveChat, showChat);
}
protected override void RemoveEventLintener()
{
NetWorkMgr.Instance.RemoveListener(Keys.ReceiveChat, showChat);
}
private void showChat(SocketIOEvent data) {
string id = data.data["id"].ToString();
string message = data.data["message"].ToString();
SpawnItem().Init(id, message);
}
private ChatItem SpawnItem()
{
GameObject prefab = Resources.Load(Paths.ChatItem);
GameObject item = Instantiate(prefab, _content);
return item.AddComponent();
}
}
SpawnItem方法中导入的预制体是一个UI组件,下面有三个文本,结构如下
Name、Time、Content都挂载着ContentSizeFitter来使大小可以自动适应。
然后
#ChatItem.cs
public class ChatItem : MonoBehaviour
{
public void Init(string id,string message)
{
RectTransform nameTrans = transform.Find("Name").GetComponent();
RectTransform TimeTrans = transform.Find("Time").GetComponent();
RectTransform ContentTrans = transform.Find("Content").GetComponent();
SetTextContent(nameTrans, id);
SetTextContent(ContentTrans, message);
SetTextContent(TimeTrans, DateTime.Now.ToString("HH:mm:ss"));
}
private void SetTextContent(Transform trans, string content)
{
//给对应位置写对应文字
trans.GetComponent().text=content;
}
}
重写一下ChatItem.cs
public class ChatItem : MonoBehaviour
{
public void Init(string id,string message)
{
RectTransform nameTrans = transform.Find("Name").GetComponent();
RectTransform TimeTrans = transform.Find("Time").GetComponent();
RectTransform ContentTrans = transform.Find("Content").GetComponent();
SetTextContent(nameTrans, id);
SetTextContent(ContentTrans, message);
SetTextContent(TimeTrans, DateTime.Now.ToString("HH:mm:ss"));
SetHeight(nameTrans, ContentTrans);
}
private void SetTextContent(Transform trans, string content)
{
//给对应位置写对应文字
trans.GetComponent().text=content;
}
private void SetHeight(RectTransform nameTrans, RectTransform ContentTrans) {
StartCoroutine(wait(nameTrans, ContentTrans));
}
private IEnumerator wait(RectTransform nameTrans, RectTransform ContentTrans) {
//挂起一帧后才会有高度偏差
yield return null;
float height = 0;
height += Math.Abs(nameTrans.sizeDelta.y);
height += Math.Abs(ContentTrans.sizeDelta.y);
RectTransform self = GetComponent();
print(height);
self.sizeDelta = new Vector2(self.sizeDelta.x,height+40);
}
}
这个协程的作用是让背景跟随输入文字的多少实现自适应,预制体要注意:
下面我做了一些修改(躺了几个大坑)
到现在,我们来整理一下Game场景的目录结构
Chat下面由ShowMessage和SendMessage,其中ShowMessage下面是一个UI——Scroll View,挂在组件:
Scroll View下面我把Scroll Bar都删了,现在只有一个ViewPort(滚动视野),上面挂载着Mask来挡住content的内容
最后是content,承载聊天内容的面板,我们的预制体就是会生成在它下面,它挂载着Grid Layout Group,使得我们生成的聊天内容按照一行一个的网格布局。
最后我们改进一下代码
#ChatItem.cs
public class ChatItem : MonoBehaviour
{
public void Init(string id,string message,Action callback)
{
RectTransform nameTrans = transform.Find("Name").GetComponent();
RectTransform TimeTrans = transform.Find("Time").GetComponent();
RectTransform ContentTrans = transform.Find("Content").GetComponent();
SetTextContent(nameTrans, id);
SetTextContent(ContentTrans, message);
SetTextContent(TimeTrans, DateTime.Now.ToString("HH:mm:ss"));
SetHeight(nameTrans, ContentTrans,callback);
}
private void SetTextContent(Transform trans, string content)
{
//给对应位置写对应文字
trans.GetComponent().text=content;
}
private void SetHeight(RectTransform nameTrans, RectTransform ContentTrans,Action callback) {
StartCoroutine(wait(nameTrans, ContentTrans,callback));
}
private IEnumerator wait(RectTransform nameTrans, RectTransform ContentTrans,Action callback) {
//挂起一帧后才会有高度偏差
yield return null;
float height = 0;
height += Math.Abs(nameTrans.sizeDelta.y);
height += Math.Abs(ContentTrans.sizeDelta.y);
RectTransform self = GetComponent();
print(height);
self.sizeDelta = new Vector2(self.sizeDelta.x,height+50);
if (callback != null) {
callback();
}
}
}
#ShowMessage.cs
public class ShowMessage : ViewBase
{
private ScrollRect _scrollRect;
private Transform _content;
private void Start()
{
_content = transform.Find("Scroll View/Viewport/Content");
_scrollRect = GetComponentInChildren();
}
protected override void AddEventLintener()
{
NetWorkMgr.Instance.AddListener(Keys.ReceiveChat, showChat);
}
protected override void RemoveEventLintener()
{
NetWorkMgr.Instance.RemoveListener(Keys.ReceiveChat, showChat);
}
private void showChat(SocketIOEvent data) {
string id = data.data["id"].ToString();
string message = data.data["message"].ToString();
SpawnItem().Init(id, message, UpdateContentPosition);
}
private ChatItem SpawnItem()
{
GameObject prefab = Resources.Load(Paths.ChatItem);
GameObject item = Instantiate(prefab, _content);
return item.AddComponent();
}
private void UpdateContentPosition() {
//刷新Canvas,避免排列造成画面抖动
Canvas.ForceUpdateCanvases();
//更改标准位置,从而使得视图罩住content的下部分
//实现实时显示最新的消息的效果
_scrollRect.verticalNormalizedPosition = 0;
}
}
这样,我们就实现了消息的实时更新
上面的步骤让我们实现了可以基本进行实时聊天了,接下来我们将人物的更多信息传给客户端。首先定义一个PlayData类来存储本地Player的一些属性与一些特征
public class PlayerData
{
public static string ID { get; set; };
}
定义了ID,我们在LoginView.cs中初始化
private void LoginResult(SocketIOEvent data) {
//如果该方法被调用,说明node端向C#端发送了key为login的信息
//data是一个SocketIOEvent对象的json属性,通过下标得到值
if (Util.GetBoolFromJson(data.data["result"]))
{
//同步加载
SceneManager.LoadScene("Game");
PlayerData.ID = data.data["user"].ToString();
}
else {
Debug.Log("Login lose");
}
}
并且在SendMessage.cs中,就可以指定好
public class SendMessage : MonoBehaviour
{
void Start()
{
string message = "";
//发送消息的输入框
InputField input = transform.Find("InputField").GetComponent();
input.onEndEdit.AddListener((text) =>
{
message = text;
});
input.text = "";
//发送消息的按钮
transform.Find("Send").GetComponent
然后我们测试,会发现:哎,居然不显示用户名
问题出在哪里呢?我在发送消息按钮上绑定了一个打印封装的json的命令,会发现,它给id封装了两个双引号,所以node端没有办法解析到内容。
这个问题其实是获取json数据常见的一种问题
我这里的解决方式采用了一个插件——LitJson
LitJson是一个开源项目,比较小巧轻便,安装也很简单,在Unity里只需要把LitJson.dll放到Plugins文件夹下,并在代码的最开头添加 “Using LitJson”就可以了。简单来说,LitJson的用途是实现Json和代码数据之间的转换,一般用于从服务器请求数据,得到返回的Json后进行转换从而在代码里可以访问。
github上有源代码,我还准备了直接编译好的dll,提取码:7sat
将dll放到Unity的Plugins目录即可
好的,我们重新修改一下代码
#LoginView.cs
private void LoginResult(SocketIOEvent data) {
//如果该方法被调用,说明node端向C#端发送了key为login的信息
//data是一个SocketIOEvent对象的json属性,通过下标得到值
if (Util.GetBoolFromJson(data.data["result"]))
{
JsonData Json = JsonMapper.ToObject(data.data.ToString());
PlayerData.ID = Json["user"].ToString();
SceneManager.LoadScene("Game");
}
else {
Debug.Log("Login lose");
}
}
#ShowMessage.cs
private void showChat(SocketIOEvent data) {
JsonData Json = JsonMapper.ToObject(data.data.ToString());
string id = Json["id"].ToString();
string message = Json["message"].ToString();
SpawnItem().Init(id, message, UpdateContentPosition);
}
我们计划将移动方式为玩家点击某处从而移动,所以,我们需要导航系统,烘焙一下导航网格,还要给角色挂载寻路代理。
给Util工具类添加三个方法方便后期转换值
public static JSONObject VectorToJson(Vector3 pos) {
JSONObject json = new JSONObject(JSONObject.Type.OBJECT);
json.AddField("x",pos.x);
json.AddField("y", pos.y);
json.AddField("z", pos.z);
return json;
}
public static Vector3 JsonToVector(JSONObject json)
{
//.f可以直接获得float的值
return new Vector3(json["x"].f, json["y"].f, json["z"].f);
}
public static string GetID(SocketIOEvent data) {
//这里的获取ID方法,只能拆json包中键为id的
JsonData Json = JsonMapper.ToObject(data.data.ToString());
return Json["id"].ToString();
}
我们新建一个类,用来检测玩家的点击来让角色行走,目前写入如下代码,将人物移动信息发送给服务端(注意,这个类不能添加在预制体角色身上,因为应该玩家只可以控制自己的角色,我在StartGameView.cs中的Start方法里添加了这样一句 gameObject.AddComponent
public class PlayerClick : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButtonDown(0)) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
HitGround(hit);
}
}
}
private void HitGround(RaycastHit hit) {
//封装json
JSONObject json = new JSONObject(JSONObject.Type.OBJECT);
json.AddField("id", PlayerData.ID);
Vector3 playerPos = PlayerSpawner.Instance
.GetPlayer(PlayerData.ID)
.transform.position;
json.AddField("StartPos", Util.VectorToJson(playerPos));
json.AddField("TargetPos", Util.VectorToJson(hit.point));
NetWorkMgr.Instance.Emit(Keys.Move, json);
}
}
回到服务端,修改startGame.js,我们来处理服务端接收Keys.Move
var chatRoom=require('./Chatroom')
let players={}
module.exports.InintStartGame=function(socket,playID){
//获取当前的player对象
let player={
id:playID,
position:{x:0,y:0,z:0},
rotation:{x:0,y:0,z:0}
}
socket.on(keys.InitGameComplete,function(data){
chatRoom.InitChatRoom(socket)
//将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])
}
})
socket.on(keys.Move,function(data){
player.position.x=data.StartPos.x;
player.position.y=data.StartPos.y;
player.position.z=data.StartPos.z;
//给客户端发送
socket.emit(keys.Move,data);
//给所有客户端广播
socket.broadcast.emit(keys.Move,data);
})
}
然后我们再回到客户端,添加一个脚本Move挂载给角色来接收服务端发送的keys.Move
在写Move之前,我们先进行这样的处理,在StartGameView.cs的生成Player中,给生成的Player添加组件PlayerView,并调用它的init方法
private void spawnPlayer(SocketIOEvent data) {
//新建角色
string id = Util.GetID(data);
var player = PlayerSpawner.Instance.SpawnPlayer(id);
//PlayView这个组件会负责给Player添加其他组件
player.AddComponent().Init(id);
}
playerview的内容如下
public class PlayView : MonoBehaviour
{
public void Init(string ID)
{
gameObject.AddComponent();
Data data=gameObject.AddComponent();
data.ID = ID;
}
}
文件Data.cs的内容如下
public class Data : MonoBehaviour
{
public string ID { get; set; }
}
好,我们来写Move.cs文件
public class Move : ViewBase
{
//这也是一个视图类
private NavMeshAgent _agent;
private void Start()
{
_agent = GetComponent();
}
protected override void AddEventLintener()
{
NetWorkMgr.Instance.AddListener(Keys.Move, OnMove);
}
protected override void RemoveEventLintener()
{
NetWorkMgr.Instance.RemoveListener(Keys.Move, OnMove);
}
private void OnMove(SocketIOEvent data)
{
//Util的GetID可以得到json中id键对应的值
string id = Util.GetID(data);
Data ownData = GetComponent();
//判断是生成自己还是处理别人
if (id == ownData.ID) {
//获取PlayerClick.cs中封装的起始/目标位置
transform.position = Util.JsonToVector(data.data["StartPos"]);
_agent.SetDestination(Util.JsonToVector(data.data["TargetPos"]));
}
}
}
到这里,我们应该就可以实现基本人物移动了
缕一下:PlayerClick将用户id、起始位置、目标位置(用户点击位置)发送给了服务端的InintStartGame中的监听,这个监听将消息转发给了所有客户端。
客户端收到消息后,进行处理。
下面为了丰富游戏效果,我们先添加一下动画,这个相信大家应该都会的。
我这里是添加了一个混合树
动画的播放由变量distance决定
在PlayerView中再让它自动添加一个组件——AniController
public void Init(string ID)
{
gameObject.AddComponent();
Data data=gameObject.AddComponent();
gameObject.AddComponent();
data.ID = ID;
}
AniController的内容如下
public class AniController : MonoBehaviour
{
private Animator _animator;
private NavMeshAgent _agent;
private int _timer;
private int _distanceID = Animator.StringToHash("distance");
void Start()
{
_animator = GetComponent();
_agent = GetComponent();
_timer = 0;
}
void Update()
{
//每20帧检测一次,限制执行频率
if (_timer > 20)
{
//通过与目标位置距离来设置状态机的变量distance
_animator.SetFloat(_distanceID, _agent.remainingDistance);
_timer = 0;
}
_timer++;
}
}
然后我们添加一下镜头跟随效果
新建脚本FollowPlayer
public class FollowPlayer : MonoBehaviour
{
//处理相机对人物的跟随
public Transform Player;
public float rotateSpeed = 5;
private bool isRotating = false; //标志
private Vector3 offsetPosition; //一个位置偏移
private void Start()
{
//相机朝向人物位置
transform.LookAt(Player.transform.position);
//记录人物与相机的位置偏移
offsetPosition = transform.position - Player.position;
}
private void Update()
{
//永远保持偏移距离
transform.position = Player.transform.position + offsetPosition;
RotateView();
}
void RotateView()
{
//Input.GetAxis("Mouse X"); //得到鼠标在水平方向的滑动
//Input.GetAxis("Mouse Y"); //得到鼠标在竖直方向的滑动
if (Input.GetMouseButtonDown(1)) //按下鼠标右键
{
isRotating = true;
}
if (Input.GetMouseButtonUp(1))
{
isRotating = false;
}
if (isRotating)
{
//绕某一点,某一轴旋转(相机初始化会朝向人物,所以尽量将
//人物与相机放在相同x轴上,z值为0)
transform.RotateAround(Player.position, Player.up, rotateSpeed * Input.GetAxis("Mouse X"));
//对上下方向的旋转要做一个范围限制
//保存原来的数据(position,rotation)
Vector3 originalPos = transform.position;
Quaternion originalRot = transform.rotation;
transform.RotateAround(Player.position, transform.right, -rotateSpeed * Input.GetAxis("Mouse Y"));
//得到x的旋转
float x = transform.eulerAngles.x;
if (x < 10 || x > 80) //超出范围,还原这一步旋转
{
transform.position = originalPos;
transform.rotation = originalRot;
}
}
//更新offsetPosition
offsetPosition = transform.position - Player.position;
}
}
然后在我们的PlayView.cs中添加
public class PlayView : MonoBehaviour
{
public void Init(string ID)
{
gameObject.AddComponent();
Data data=gameObject.AddComponent();
gameObject.AddComponent();
data.ID = ID;
//现在新建的角色的id就是登陆的玩家的id
if (ID == PlayerData.ID) {
//给主相机挂载相机跟随人物的脚本
Camera.main.gameObject.AddComponent()
.Player = PlayerSpawner.Instance
.GetPlayer(PlayerData.ID).transform;
}
}
}
然后就OK了!
我们导出一个客户端来测试一波
相信大家也看出来了,我们现在没有断线逻辑。
如果我们登陆又退出,是会出现问题的。
首先,先核对一下服务端和客户端两边的Keys,确保一致
var keys={
Connection:"connection",
Disconnection : "disconnect",
OtherDisconnect : "otherDisconnect",
Login : "login",
InitGameComplete : "initgamecomplete",
Chat : "chat",
ReceiveChat : "receivechat",
Spawn : "spawn",
Move : "move",
Follow : "follow",
UpdatePosition : "updateposition",
Attack : "attack"
}
在服务端的StartGame.js中加了一个对Disconnection的监听,完整文件如下:
var chatRoom=require('./Chatroom')
let players={}
module.exports.InintStartGame=function(socket,playID){
//获取当前的player对象
let player={
id:playID,
position:{x:0,y:0,z:0},
rotation:{x:0,y:0,z:0}
}
socket.on(keys.InitGameComplete,function(data){
chatRoom.InitChatRoom(socket)
//将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])
}
})
socket.on(keys.Move,function(data){
player.position.x=data.StartPos.x;
player.position.y=data.StartPos.y;
player.position.z=data.StartPos.z;
//给客户端发送
socket.emit(keys.Move,data);
//给所有客户端广播
socket.broadcast.emit(keys.Move,data);
})
//这里的键值是socketio定义好的关键字,检测下线
socket.on(keys.Disconnection,function(data){
delete players[playID]
//把下线的id传给其他客户端
socket.broadcast.emit(keys.OtherDisconnect,{id:playID})
})
}
下面是connectView.cs,很早之前的文件
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);
NetWorkMgr.Instance.AddListener(Keys.OtherDisconnect, OtherDisConnect);
}
private void OnDestroy()
{
NetWorkMgr.Instance.RemoveListener(Keys.Connection, Connect);
NetWorkMgr.Instance.RemoveListener(Keys.Disconnection, DisConnect);
NetWorkMgr.Instance.RemoveListener(Keys.OtherDisconnect, OtherDisConnect);
}
//建立连接的方法
private void Connect(SocketIOEvent data)
{
Debug.Log("connect server Success");
}
//断开链接的方法
private void DisConnect(SocketIOEvent data) {
PlayerSpawner.Instance.RemovePlayer(PlayerData.ID);
}
private void OtherDisConnect(SocketIOEvent data)
{
string id = Util.GetID(data);
PlayerSpawner.Instance.RemovePlayer(id);
}
}
这个在服务端做手脚就好了
我们在StartGame.js中记录了当前的登录用户,所以我们先将这个对象设置成全局对象
var players={}
global.players=players
然后在login.js中,CheckAccount方法里加一句检测条件即可
function CheckAccount(data){
var result=false;
testAccount.forEach(function(Item){
if(Item.user==data.user&&Item.passwd==data.passwd&&!players.hasOwnProperty(data.user)){
result=true
}
})
return result
}
我们发送消息,是通过socket去发送,通过socket的id来辨识自己该不该接收这个消息。
例如:
A给服务端发送消息,socket的ID是1,服务端返回消息,socket的ID是2,A就会接收。如果socket的ID是1,A就不会接收,因为它以为那是自己发出去的消息。
我们的服务端的聊天室功能,源代码是这样的
module.exports.InitChatRoom=function(socket){
socket.on(keys.Chat,(data)=>{
//给自己发送这个消息
socket.emit(keys.ReceiveChat,data)
//给其余人发送这个消息
socket.broadcast.emit(keys.ReceiveChat,data)
})
}
现在你懂socket.broadcast.emit的原理了吧,就是发送一个socket的id等同于自身的消息,这样,别的客户端就会接收到,我自身的客户端不会接收。那么我们要实现群发,实际就可以直接修改id
但是这个不建议这样操作,只是了解即可。
siki的这个课程到这里就追完了,人家还有个第二季,过几天再开始追那个吧。
商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢