Unity游戏开发终极实战值MMORPG Part2


基础系统

UI框架设设计

UI分类:

UIMain:主UI,只可能隐藏永远不销毁。
UIManager:各个游戏系统的UI,用不到的时候不会加载
UITipsManager:动态UI
UIMessagerBox:用户用来显示提示

UIManager:
管理所有UI的脚本,UI需要在其中注册,才能非常方便使用

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Text;
using System;

public class UIManager : Singleton<UIManager>
{
    class UIElement
    {
        public string Resources;
        public bool Cache;
        public GameObject Instance;
    }
    private Dictionary<Type, UIElement> UIResources = new Dictionary<Type, UIElement>();
    public UIManager()
    {
        // 需要加载的UI要预先添加到管理器中
        this.UIResources.Add(typeof(UITest), new UIElement() { Resources = "UI/UITest", Cache = true });
    }

    ~UIManager()
    {

    }
    // 打开UI
    public T Show<T>()
    {
        //SoundManager.Instance.PlaySOund("ui_open");
        // 如果字典里有这个Key,查找这个实例,有的话激活,没有的话生成
        Type type = typeof(T);
        if (this.UIResources.ContainsKey(type))
        {
            UIElement info = this.UIResources[type];
            if(info.Instance != null){
                info.Instance.SetActive(true);
            }
            else
            {
                UnityEngine.Object prefab = Resources.Load(info.Resources);
                if(prefab == null)
                {
                    return default(T);
                }
                info.Instance = (GameObject)GameObject.Instantiate(prefab);
            }
            return info.Instance.GetComponent<T>();

        }
        return default(T);
    }

    // 关闭UI,启用了Cache不销毁但是隐藏
    public void Close(Type type)
    {
        //SoundManager.Instance.PlaySound("ui_close");

        if (this.UIResources.ContainsKey(type))
        {
            UIElement info = this.UIResources[type];
            if (info.Cache)
            {
                info.Instance.SetActive(false);
            }
            else
            {
                GameObject.Destroy(info.Instance);
                info.Instance = null;
            }
        }
    }
}

UIWindow:
UI的父类,继承之后几乎不用谢代码就能拥有基本功能。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System;

// 所有UI的父类
public abstract class UIWindow : MonoBehaviour
{
    public delegate void CloseHanlder(UIWindow sender, WindowResult result);
    public event CloseHanlder OnClose;
    public virtual Type Type { get { return this.GetType(); } }

    public enum WindowResult
    {
        None = 0,
        Yes,
        No,
    }
    // 关键方法,无论是点击确认还是取消,都需要关闭窗口,调用该函数
    public void Close(WindowResult result = WindowResult.None)
    {
        UIManager.Instance.Close(this.Type);
        if (this.OnClose != null)
            this.OnClose(this, result);
        this.OnClose = null;
    }
    // 点击关闭(取消)按钮
    public virtual void OnCloseClick()
    {
        this.Close();
    }
    // 点击确认按钮
    public virtual void OnYesClick()
    {
        this.Close(WindowResult.Yes);
    }

    private void OnMouseDown()
    {
        Debug.LogFormat(this.name + "Clicked");
    }

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UITest : UIWindow
{
    public Text title;
    public Text content;
    void Start()
    {
        
    }

    public void SetTitle(string title)
    {
        this.title.text = title;
    }
    public void SetContent(string content)
    {
        this.content.text = content;
    }
}

使用:

UITest test = UIManager.Instance.Show<UITest>();  // 赋值之后,可以随意调用方法,更新其中的内容,添加事件等
test.SetContent("欢迎来到极世界,内容更新中,敬请期待!");
test.OnClose += Test_OnClose;
private void Test_OnClose(UIWindow sender,UIWindow.WindowResult result)
{
    Debug.LogFormat("点击了 {0}", result);
}

NPC系统

添加配置表

  1. 在Data/Table目录下新建一个配置表,填好对应的信息
  1. 重新生成txt文件,并往客户端和服务器都复制一份

  2. 在Common工程中的Data文件夹新建一个NpcDefine.cs文件,定义数据,重新生成解决方案,并同步一下dll文件

代码

NPCManager:管理NPC的注册与事件的调用

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common.Data;

namespace Managers
{
    class NPCManager:Singleton<NPCManager>
    {
        public delegate bool NpcActionHandler(NpcDefine npc);
        Dictionary<NpcFunciton, NpcActionHandler> eventMap = new Dictionary<NpcFunciton, NpcActionHandler>();

        // 注册事件,是NPC系统和其他系统的唯一接口
        public void RegisterNpcEvent(NpcFunciton function, NpcActionHandler action)
        {
            if (!eventMap.ContainsKey(function))
            {
                eventMap[function] = action;
            }
            else
            {
                eventMap[function] += action;
            }
        }

        public NpcDefine GetNpcDefine(int npcID)
        {
            return DataManager.Instance.Npcs[npcID];
        }

        public bool Interractive(int npcId)
        {
            if (DataManager.Instance.Npcs.ContainsKey(npcId))
            {
                var npc = DataManager.Instance.Npcs[npcId];
                return Interractive(npc);
            }
            return false;
        }

        public bool Interractive(NpcDefine npc)
        {
            if(npc.Type == NpcType.Task)
            {
                return DoTaskInteractive(npc);
            }
            else if (npc.Type == NpcType.Functional)
            {
                return DoFunctionInteractive(npc);
            }
            return false;
        }
        // 功能型NPC
        private bool DoFunctionInteractive(NpcDefine npc)
        {
            if (npc.Type != NpcType.Functional)
                return false;
            if (!eventMap.ContainsKey(npc.Function))
                return false;
            // 调用注册的事件
            return eventMap[npc.Function](npc);
        }
        // 任务型NPC
        private bool DoTaskInteractive(NpcDefine npc)
        {
            MessageBox.Show("点击了NPC" + npc.Name, "NPC对话");
            return true;
        }
    }
}

NPCController:控制NPC的动作,并调用Manager

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Common.Data;
using Managers;
using Models;

public class NPCController : MonoBehaviour
{
    public int npcID;
    Animator anim;
    NpcDefine npc;
    SkinnedMeshRenderer renderer;
    Color orignColor;
    private bool inInteractive = false;

    void Start()
    {
        renderer = gameObject.GetComponentInChildren<SkinnedMeshRenderer>();
        anim = gameObject.GetComponent<Animator>();
        npc = NPCManager.Instance.GetNpcDefine(npcID);
        orignColor = renderer.sharedMaterial.color;
        this.StartCoroutine(Actions());
    }
  
    IEnumerator Actions()
    {
        while (true)
        {
            if (inInteractive)
                yield return new WaitForSeconds(2f);
            else
                yield return new WaitForSeconds(Random.Range(5f, 10f));
            this.Relax();
        }
    }

    void Relax()
    {
        anim.SetTrigger("Relax");
    }
    // 交互,面向玩家,并更改动画,NPCManager执行实际功能
    void Interactive()
    {
        if (!inInteractive)
        {
            inInteractive = true;
            StartCoroutine(DoInteractive());
        }
    }

    IEnumerator DoInteractive()
    {
        yield return FaceToPlayer();
        if (NPCManager.Instance.Interractive(npc))
        {
            anim.SetTrigger("Talk");
        }
        yield return new WaitForSeconds(3f);
        inInteractive = false;
    }

    IEnumerator FaceToPlayer()
    {
        Vector3 faceTo = (User.Instance.CurrentCharacterObject.transform.position - this.transform.position).normalized;
        while (Mathf.Abs(Vector3.Angle(gameObject.transform.forward,faceTo)) > 5)
        {
            gameObject.transform.forward = Vector3.Lerp(this.gameObject.transform.forward, faceTo, Time.deltaTime * 5f);
            yield return null;
        }
    }

    private void OnMouseDown()
    {
        Interactive();
    }

    private void OnMouseOver()
    {
        HighLight(true);
    }
    private void OnMouseEnter()
    {
        HighLight(true);
    }
    private void OnMouseExit()
    {
        HighLight(false);
    }

    void HighLight(bool highlight)
    {
        if (highlight)
        {
            if (renderer.sharedMaterial.color != Color.white)
                renderer.sharedMaterial.color = Color.white;
        }
        else
        {
            if (renderer.sharedMaterial.color != orignColor)
            {
                renderer.sharedMaterial.color = orignColor;
            }
        }

    }
}

TestManager:调用测试案例

using System;

using Common.Data;
using UnityEngine;

namespace Managers
{
    class TestManager:Singleton<TestManager>
    {
        public void Init()
        {
            // 注册事件
            NPCManager.Instance.RegisterNpcEvent(NpcFunciton.InvokeInsrance, OnNPCInvokeInsrance);
            NPCManager.Instance.RegisterNpcEvent(NpcFunciton.InvokeShop, OnNpcInvokeShop);
        }

        private bool OnNpcInvokeShop(NpcDefine npc)
        {
            Debug.LogFormat("TestManager.OnNpcInvokeShop NPC:[{0}:{1}] Func:{2}",npc.ID, npc.Type, npc.Function);
            UITest test = UIManager.Instance.Show<UITest>();
            test.SetTitle(npc.Name);
            test.SetContent("你好啊");
            return true;
        }

        private bool OnNPCInvokeInsrance(NpcDefine npc)
        {
            Debug.LogFormat("TestManager.OnNPCInvokeInsrance NPC:[{0}:{1}] Func:{2}", npc.ID, npc.Type, npc.Function);
            MessageBox.Show("点击了NPC:" + npc.Name, "NPC对话");
            return true;
        }
    }
}

道具系统

需求分析

道具系统的对外接口:

设计图:

数据结构添加

  1. 在Common中新建ItemDefine添加字段,重新生成解决方案,并同步dll
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Common.Data
{
    public enum ItemFunction
    {
        RecoverHP,
        RecoverMP,
        AddBuff,
        AddExp,
        AddMoney,
        AddItem,
        AddSkillPoint,
    }
    class ItemDefine
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public ItemType Type { get; set; }
        public string Category { get; set; }
        public bool CanUse { get; set; }
        public int Price { get; set; }
        public int SellPrice { get; set; }
        public ItemFunction Function { get; set; }
        public int Param { get; set; }
        public List<int> Params { get; set; }
    }
}
  1. 编写和更改协议,加上NItemInfo的Message,并将这个message添加到NCharacterInfo这个message下,并将NCharacterInfo添加到UserGameEnterResponse下,并用工具重新生成协议

  2. 更改第三个数据结构:数据库。新增一个表ICharaterItem,并与TCharacter建立关联。并根据模型生成数据库。生成数据库后,把Entities.edmx.sql拖到SSMS中执行即可更新数据库。

代码

服务端

Item:单个道具

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GameServer.Models
{
    class Item
    {
        TCharacterItem dbItem;

        public int ItemID;
        public int Count;
        public Item(TCharacterItem item)
        {
            this.dbItem = item;
            this.ItemID = (short)item.ItemID;
            this.Count = (short)item.ItemCount;
        }

        public void Add(int count)
        {
            this.Count += count;
            dbItem.ItemCount = this.Count;
        }

        public void Remove(int count)
        {
            this.Count -= count;
            dbItem.ItemCount = this.Count;
        }

        public bool Use(int count = 1)
        {
            return false;
        }
        // 重载,方便输出
        public override string ToString()
        {
            return string.Format("ID:{0},Count:{1}", this.ItemID, this.Count);
        }

    }
}

ItemManager:一个玩家所有道具的管理

using Common;
using GameServer.Entities;
using GameServer.Models;
using GameServer.Services;
using SkillBridge.Message;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GameServer.Managers
{
    class ItemManager  // 不能做单例,角色创建才能创建
    {
        Character Owner;

        public Dictionary<int, Item> Items = new Dictionary<int, Item>();
        // 把数据库中的所有物品都添加入字典中,不用每次都查数据库
        public ItemManager(Character owner)
        {
            this.Owner = owner;
            foreach (var item in owner.Data.Items)
            {
                this.Items.Add(item.ItemID, new Item(item));
            }
        }
        // 数量不足或没有这个道具时使用失败,使用成功则扣除道具数量
        public bool UseItem(int itemId,int count = 1)
        {
            Log.InfoFormat("[{0}]UseItem[{1}:{2}]", this.Owner.Data, itemId, count);
            Item item = null;
            if(this.Items.TryGetValue(itemId,out item))
            {
                if (item.Count < count)
                    return false;

                // TODO:增加使用逻辑
                item.Remove(count);
                return true;
            }
            return false;
        }
        // 下面的两个环境逻辑相同,但应用环境不同,为了提交运行效率,宁可让代码多一点,不增加多余函数
        public bool HasItem(int itemId)
        {
            Item item = null;
            if (this.Items.TryGetValue(itemId, out item))
                return item.Count > 0;
            return false;
        }
        public Item GetItem(int itemId)
        {
            Item item = null;
            this.Items.TryGetValue(itemId, out item);
            Log.InfoFormat("[{0}]GetItem[{1}:{2}]", this.Owner.Data.ID, itemId, item);
            return item;
        }
        // 如果有这个道具,就直接在本地添加数据,如果没有,需要添加新的值
        public bool AddItem(int itemId,int count)
        {
            Item item = null;
            if(this.Items.TryGetValue(itemId,out item))
            {
                item.Add(count);
            }
            else
            {
                TCharacterItem dbItem = new TCharacterItem();
                dbItem.CharacterID = Owner.Data.ID;
                dbItem.Owner = Owner.Data;
                dbItem.ItemID = itemId;
                dbItem.ItemCount = count;
                Owner.Data.Items.Add(dbItem);
                item = new Item(dbItem);
                this.Items.Add(itemId, item);
            }
            Log.InfoFormat("[{0}]AddItem[{1} addCount:{2}]", this.Owner.Data.ID, item, count);
            // 把本地内存中的数据库数据真正保存到数据库
            DBService.Instance.Save();
            return true;
        }

        public bool RemoveItem(int itemId, int count)
        {
            if (!this.Items.ContainsKey(itemId))
            {
                return false;
            }
            Item item = this.Items[itemId];
            if (item.Count < count)
                return false;
            item.Remove(count);
            Log.InfoFormat("[{0}]RemoveItem[{1} RemoveCount:{2}]", this.Owner.Data.ID, item, count);
            DBService.Instance.Save();
            return true;
        }
        // 方便从内存数据转移到网络数据
        public void GetItemInfos(List<NItemInfo> list)
        {
            foreach (var item in this.Items)
            { 
                list.Add(new NItemInfo() { Id = item.Value.ItemID, Count = item.Value.Count });
            }
        }
    }
}

UserService新增:

void OnGameEnter(NetConnection<NetSession> sender, UserGameEnterRequest request)
{
    message.Response.gameEnter.Character = character.Info;
}

客户端

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SkillBridge.Message;

namespace Models
{
    public class Item
    {
        public int id;
        public int Count;

        public Item(NItemInfo item)
        {
            this.id = item.Id;
            this.Count = item.Count;
        }

        public override string ToString()
        {
            return string.Format("Id:{0},Count:{1}", this.id, this.Count);
        }
    }
}
using System;
using System.Collections.Generic;
using Models;
using SkillBridge.Message;
using UnityEngine;
using Common.Data;

namespace Managers
{
    class ItemManager:Singleton<ItemManager>
    {
        public Dictionary<int, Item> Items = new Dictionary<int, Item>();
        internal void Init(List<NItemInfo> items)
        {
            this.Items.Clear();
            foreach (var info in items)
            {
                Item item = new Item(info);
                this.Items.Add(item.id, item);
                Debug.LogFormat("ItemManager:Init[{0}]", item);
            }

        }
        
        public ItemDefine GetItem(int itemId)
        {
            return null;
        }
        public bool UseItem(int itemId)
        {
            return false;
        }

        public bool UseItem(ItemDefine item)
        {
            return false;
        }
    }
}

背包系统

注意:需要使用Unsafe的代码,Edit->Project Setting->Player->other Setting中可以设置

数据的添加

传输协议

// 新增11
message NCharacterInfo {
	NBagInfo Bag = 11;
}
message NBagInfo{
	int32 unlocked = 1;   // 解锁了几个格子
	bytes Items = 2;      // 长度不定,决定了每个格子存放什么
}
message BagSaveRequest{
    NBagInfo BagInfo = 1;
}
message BagSaveResponse{
    RESULT result = 1;
    string errormsg = 2;
}

代码

服务端

服务端代码比较简单,保存一下就好

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SkillBridge.Message;
using Common;
using Common.Data;
using Network;
using System.Threading.Tasks;
using GameServer.Entities;

namespace GameServer.Services
{
    class BagService:Singleton<BagService>
    {
        public BagService()
        {
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<BagSaveRequest>(this.OnBagSave);
        }

        public void Init()
        {

        }
        private void OnBagSave(NetConnection<NetSession> sender, BagSaveRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("BagSaveRequest: character:{0}:Unlocked{1}", character.Id, request.BagInfo.Unlocked);

            if(request.BagInfo != null)
            {
                character.Data.Bag.Items = request.BagInfo.Items;
                DBService.Instance.Save();
            }
        }
    }
}

客户端

客户端代码就比较讲究了,首先需要自己拼好背包的UI,这个不赘述。代码如下:
单个物品UI代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIIconItem : MonoBehaviour
{
    public Image mainImage;
    public Image secondImage;
    public Text mainText;

    public void SetMainIcon(string iconName,string text)
    {
        this.mainImage.overrideSprite = Resloader.Load<Sprite>(iconName);
        if (text == "1")
            this.mainText.text = null;
        else
            this.mainText.text = text;
    }
}

背包单个格子的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

namespace Models
{
    // 结构布局,代表这个结构体在内存中存储格式,不用类的原因是值类型方便交换格子
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    struct BagItem
    {
        public ushort ItemId;
        public ushort Count;

        public static BagItem zero = new BagItem { ItemId = 0, Count = 0 };

        public BagItem(int itemId, int count)
        {
            this.ItemId = (ushort)itemId;
            this.Count = (ushort)count;
        }
        public static bool operator ==(BagItem lhs, BagItem rhs)
        {
            return lhs.ItemId == rhs.ItemId && lhs.Count == rhs.Count;
        }
        public static bool operator !=(BagItem lhs, BagItem rhs)
        {
            return !(lhs == rhs);
        }

        public override bool Equals(object other)
        {
            if(other is BagItem)
            {
                return Equals((BagItem)other);
            }
            return false;
        }
        public bool Equals(BagItem other)
        {
            return this == other;
        }

        public override int GetHashCode()
        {
            return ItemId.GetHashCode() ^ (Count.GetHashCode() << 2);
        }


    }
}

背包逻辑代码(重要)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Models;
using SkillBridge.Message;

namespace Managers
{
    class BagManager:Singleton<BagManager>
    {
        public int Unlocked;
        public BagItem[] Items;

        NBagInfo info;
        unsafe public void Init(NBagInfo info)
        {
            this.info = info;
            this.Unlocked = info.Unlocked;
            Items = new BagItem[this.Unlocked];
            // 如果有服务器的网络信息,就赋值给本地的BagItem数组,否则是第一次登录,重新生成
            if(info.Items != null && info.Items.Length >= this.Unlocked)
            {
                Analyze(info.Items);
            }
            else
            {
                info.Items = new byte[sizeof(BagItem) * this.Unlocked];
                Reset();
            }
        }
        // 道具整理
        public void Reset()
        {
            int i = 0;
            foreach (var kv in ItemManager.Instance.Items)
            {
                // 小于堆叠数量则直接填到背包
                if (kv.Value.Count <= kv.Value.Define.StackLimit)
                {
                    this.Items[i].ItemId = (ushort)kv.Key;
                    this.Items[i].Count = (ushort)kv.Value.Count;
                }
                else   // 大于堆叠数量,做拆分
                {
                    int count = kv.Value.Count;
                    while (count >kv.Value.Define.StackLimit)
                    {
                        this.Items[i].ItemId = (ushort)kv.Key;
                        this.Items[i].Count = (ushort)kv.Value.Define.StackLimit;
                        i++;
                        count -= kv.Value.Define.StackLimit;
                    }
                    this.Items[i].ItemId = (ushort)kv.Key;
                    this.Items[i].Count = (ushort)count;
                }
                i++;
            }
        }
        // 字节数组解析成结构体数组(背包数组)
        unsafe void Analyze(byte[] data)          
        {
            // C# 中想使用指针必须在fixed内,否则C#补鞥呢保证地址是确定的
            fixed(byte* pt = data)
            {
                for (int i = 0; i < this.Unlocked; i++)
                {
                    BagItem* item = (BagItem*)(pt + i * sizeof(BagItem));    // 操作地址,赋值给BagItem的地址
                    Items[i] = *item;                                        //把地址对应的值背包的结构体数组,因为是结构体赋值时地址不变,值改变

                }
            }
        }
        // 这里不同的地方在于把背包数组的值赋回给了字节数组,用于发给服务器
        unsafe public NBagInfo GetBagInfo()
        {
            fixed(byte* pt = info.Items)
            {
                for (int i = 0; i < this.Unlocked; i++)
                {
                    BagItem* item = (BagItem*)(pt + i * sizeof(BagItem));
                    *item = Items[i];                                
                }
            }
            return this.info;
        }
    }
}

背包UI层代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using SkillBridge.Message;
using Models;
using Managers;
using System;

public class UIBag : UIWindow
{
    public Text money;
    public Transform[] pages;
    public GameObject bagItem;
    List<Image> slots;


    void Start()
    {
        if(slots == null)
        {
            slots = new List<Image>();
            for (int page = 0; page < this.pages.Length; page++)
            {
                slots.AddRange(this.pages[page].GetComponentsInChildren<Image>(true));
            }
        }
        StartCoroutine(InitBags());
    }

    IEnumerator InitBags()
    {
        // 激活的格子放置指定物品
        for (int i = 0; i < BagManager.Instance.Items.Length; i++)
        {
            var item = BagManager.Instance.Items[i];
            if (item.ItemId > 0)
            {
                GameObject go = Instantiate(bagItem, slots[i].transform);
                var ui = go.GetComponent<UIIconItem>();
                var def = ItemManager.Instance.Items[item.ItemId].Define;
                ui.SetMainIcon(def.Icon, item.Count.ToString());
            }
        }
        // 没有激活的格子变成灰色
        for (int i = BagManager.Instance.Items.Length; i < slots.Count; i++)
        {
            slots[i].color = Color.gray; ;
        }
        yield return null;
    }
    // 设置金钱
    public void SetTitle(string title)
    {
        this.money.text = User.Instance.CurrentCharacter.Id.ToString();
    }
    // 整理背包按钮
    public void OnReset()
    {
        BagManager.Instance.Reset();
    }
}

Service层BagService代码(待添加)

商店系统

需求分析

添加数据

  1. 配置表新增ShopDefine和ShopItemDefine
  2. 协议新增NcharacterInfo的gold,还添加新协议ItemBuyRequest和ItemBuyResponse。
  3. 数据库新加金币字段Gold

添加协议注意事项

每次加完协议之后要在MessageDispath中加上消息分发。

状态系统

这里进行了代码的重构,客户端发送一条数据到服务端的时候,往往牵涉到很多个系统,这个时候系统往往要回复多条数据,例如:购买商品要回复:购买是否成功,金钱,道具数量。这对于代码编写和扩展有些不变,这里的想法是做一个能将所有信息合并在一起发送的系统。

数据添加

1.添加协议

enum STATUS_ACTION
{
	UPTATE = 0;
	ADD = 1;
	DELETE = 2;
}

enum STATUS_TYPE
{
	MONEY = 0;
	EXP = 1;
	SKILL_POINT = 2;
	ITEM = 3;
}
// 先忽略
enum STATUS_SOURCE
{
	UPTATE = 0;
	ADD = 1;
	DELETE = 2;
}

message NStatus{
	STATUS_TYPE type = 1;
	STATUS_ACTION action = 2;
	int32 id = 3;
	int32 value = 4;
}
// 这里可以通过一次通知把所有变化加进去
message StatusNotify{
	repeated NStatus status = 1;
}
// 新增的
message NetMessageRequest{
	ItemBuyRequest itemBuy = 10;
}
message NetMessageResponse{	
	ItemBuyResponse itemBuy = 10;
	StatusNotify statusNotify = 100;
}
  1. 在服务器和客户端的Character添加金钱数据

服务器代码

底层架构更改
NetConnection新增

public void SendResponse()
{
    byte[] data = session.GetResponse();
    this.SendData(data, 0, data.Length);
}

NetSession新增

public byte[] GetResponse()
{
    if (response != null)
    {
        if (this.Character != null && this.Character.StatusManager.HasStatus)
        {
            this.Character.StatusManager.ApplyResponse(Response);       // 这里把所有的状态通知打包进来了
        }
        byte[] data = PackageHandler.PackMessage(response);
        response = null;   // 会话一结束清空信息
        return data;
    }
    return null;
}

新增类StatusManager:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GameServer.Entities;
using SkillBridge.Message;

namespace GameServer.Managers
{
    class StatusManager
    {
        Character Owner;
        private List<NStatus> Status { get; set; }

        public bool HasStatus
        {
            get { return this.Status.Count > 0; }
        }
        public StatusManager(Character owner)
        {
            this.Owner = owner;
            this.Status = new List<NStatus>();
        }
        // 添加各种状态信息
        public void AddStatus(StatusType type,int id,int value,StatusAction action)
        {
            this.Status.Add(new NStatus()
            {
                Type = type,
                Id = id,
                Value = value,
                Action = action
            });
        }

        public void AddGoldChange(int goldDelta)
        {
            if (goldDelta > 0)
            {
                this.AddStatus(StatusType.Money, 0, goldDelta, StatusAction.Add);
            }
            if (goldDelta < 0)
            {
                this.AddStatus(StatusType.Money, 0, -goldDelta, StatusAction.Delete);
            }
        }

        public void AddItemChange(int id,int count,StatusAction action)
        {
            this.AddStatus(StatusType.Item, id, count, action);
        }

        // 通过这个方法可以把所有状态变化放到StatusNotify中发出去
        public void ApplyResponse(NetMessageResponse message)
        {
            if (message.statusNotify == null)
                message.statusNotify = new StatusNotify();
            foreach (var status in this.Status)
            {
                message.statusNotify.Status.Add(status);
            }
            this.Status.Clear();
        }
    }
}

客户端代码

新增StatusService,接收状态的返回信息

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Models;
using Network;
using SkillBridge.Message;

namespace Services
{
    class StatusService : Singleton<StatusService>, IDisposable
    {
        public delegate bool StatusNotifyHandler(NStatus status);
        Dictionary<StatusType, StatusNotifyHandler> eventMap = new Dictionary<StatusType, StatusNotifyHandler>();

        public void Init()
        {

        }
        // 注册事件
        public void RegisterStatusNofity(StatusType function, StatusNotifyHandler action)
        {
            if (!eventMap.ContainsKey(function))
            {
                eventMap[function] = action;
            }
            else
            {
                eventMap[function] += action;
            }
        }

        public StatusService()
        {
            MessageDistributer.Instance.Subscribe<StatusNotify>(this.OnStatusNotify);
        }
        public void Dispose()
        {
            MessageDistributer.Instance.Unsubscribe<StatusNotify>(this.OnStatusNotify);
        }
        // 服务器的响应函数
        private void OnStatusNotify(object sender, StatusNotify notify)
        {
            foreach (NStatus status in notify.Status)
            {
                Notify(status);
            }
        }
        // 根据每一种收到的状态,执行不同的操作
        private void Notify(NStatus status)
        {
            Debug.LogFormat("StatusNotify:[{0}][{1}]{2}:{3}", status.Type, status.Action, status.Id, status.Value);

            if (status.Type == StatusType.Money)
            {
                // 是钱直接加,不是钱发通知
                if (status.Type == StatusType.Money)
                {
                    if (status.Action == StatusAction.Add)
                        User.Instance.AddGold(status.Value);
                    else if (status.Action == StatusAction.Delete)
                        User.Instance.AddGold(-status.Value);
                }
                StatusNotifyHandler handler;
                if (eventMap.TryGetValue(status.Type, out handler))
                {
                    handler(status);
                }
             
            }
        }
    }
}

登录系统的新用法:
UserServiceOnLogin改变

void OnLogin(NetConnection<NetSession> sender, UserLoginRequest request)
{
    Log.InfoFormat("UserRegisterRequest: User:{0}  Pass:{1}", request.User, request.Passward);

    // 注意这里的变化
    sender.Session.Response.userLogin = new UserLoginResponse();
    TUser user = DBService.Instance.Entities.Users.Where(u => u.Username == request.User).FirstOrDefault();
    if (user == null)
    {
        sender.Session.Response.userLogin.Result = Result.Failed;
        sender.Session.Response.userLogin.Errormsg = "用户不存在";
    }
    else if (request.Passward != user.Password)
    {
        sender.Session.Response.userLogin.Result = Result.Failed;
        sender.Session.Response.userLogin.Errormsg = "密码不正确.";
    }
    else
    {
        sender.Session.User = user;
        sender.Session.Response.userLogin.Result = Result.Success;
        sender.Session.Response.userLogin.Errormsg = "None";

        sender.Session.Response.userLogin.Userinfo = new NUserInfo();
        sender.Session.Response.userLogin.Userinfo.Id = (int)user.ID;            // 注意ID
        sender.Session.Response.userLogin.Userinfo.Player = new NPlayerInfo();
        sender.Session.Response.userLogin.Userinfo.Player.Id = user.Player.ID;
        foreach(var c in user.Player.Characters)
        {
            NCharacterInfo info = new NCharacterInfo();
            info.Id = c.ID;
            info.Name = c.Name;
            info.Type = CharacterType.Player;
            info.Class = (CharacterClass)c.Class;
            info.Tid = c.ID;
            sender.Session.Response.userLogin.Userinfo.Player.Characters.Add(info);
        }
    }
    sender.SendResponse();
}

商店系统制作

准备工作:

  1. 建立商店的UI,和背包类似

客户端代码

UI层代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using SkillBridge.Message;
using Models;
using Managers;
using System;
using Common.Data;

public class UIShop : UIWindow
{
    public Text title;
    public Text money;

    public GameObject shopItem;
    ShopDefine shop;
    public Transform[] itemRoot;

    void Start()
    {
        StartCoroutine(InitItems());
    }

    IEnumerator InitItems()
    {
        foreach (var kv in DataManager.Instance.ShopItems[shop.ID])
        {
            if (kv.Value.Status > 0)
            {
                GameObject go = Instantiate(shopItem, itemRoot[0]);       // 添加在第一页表
                UIShopItem ui = go.GetComponent<UIShopItem>();
                ui.SetShopItem(kv.Key, kv.Value, this);
            }
        }
        yield return null;
    }

    public void SetShop(ShopDefine shop)
    {
        this.shop = shop;
        this.title.text = shop.Name;
        this.money.text = User.Instance.CurrentCharacter.Gold.ToString();
    }

    private UIShopItem selectedItem;

    public void SelectShopItem(UIShopItem item)
    {
        if (selectedItem != null)
            selectedItem.Selected = false;
        selectedItem = item;
    }
    // 点击购买
    public void OnClickBuy()
    {
        if(this.selectedItem == null)
        {
            MessageBox.Show("请选择要购买的道具", "购买提示");
            return;
        }
        // TOdo:购买
        if (!ShopManager.Instance.BuyItem(this.shop.ID, this.selectedItem.ShopItemID))
        {

        }
    }
}

Manager层代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common.Data;
using Services;

namespace Managers
{
    class ShopManager : Singleton<ShopManager>
    {
        public void Init()
        {
            // 注册NPC事件
            NPCManager.Instance.RegisterNpcEvent(Common.Data.NpcFunciton.InvokeShop, OnOpenShop);
        }

        private bool OnOpenShop(NpcDefine npc)
        {
            this.ShowShop(npc.Param);   // 即使是同样性质的商店,通过不同的参数也能访问不同的商店
            return true;
        }
        // 打开商店
        public void ShowShop(int shopId)
        {
            ShopDefine shop;
            if(DataManager.Instance.Shops.TryGetValue(shopId,out shop))
            {
                UIShop uiShop = UIManager.Instance.Show<UIShop>();   // 这个UI是立刻生成创建的,会调用Start携程,生成其中物品
                if(uiShop != null)
                {
                    uiShop.SetShop(shop);
                }
            }
        }

        public bool BuyItem(int shopId,int shopItemId)
        {
            ItemService.Instance.SendBuyItem(shopId, shopItemId);
            return true;
        }
    }
}

新增ItemService:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Network;
using SkillBridge.Message;
using UnityEngine;

namespace Services
{
    class ItemService : Singleton<ItemService>,IDisposable
    {
        public ItemService()
        {
            MessageDistributer.Instance.Subscribe<ItemBuyResponse>(this.OnItemBuy);
        }
        public void Dispose()
        {
            MessageDistributer.Instance.Unsubscribe<ItemBuyResponse>(this.OnItemBuy);
        }

        public void SendBuyItem(int shopId,int shopItemId)
        {
            Debug.Log("SendBuyItem");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.itemBuy = new ItemBuyRequest();
            message.Request.itemBuy.shopId = shopId;
            message.Request.itemBuy.shopItemId = shopItemId;
            NetClient.Instance.SendMessage(message);
        }

        private void OnItemBuy(object sender,ItemBuyResponse message)
        {
            MessageBox.Show("购买结果:" + message.Result + "\n" + message.Errormsg, "购买完成");
        }
    }
}

ItemManager:

using System.Collections.Generic;
using Models;
using SkillBridge.Message;
using UnityEngine;
using Services;
using Common.Data;

namespace Managers
{
    class ItemManager:Singleton<ItemManager>
    {
        public Dictionary<int, Item> Items = new Dictionary<int, Item>();
        internal void Init(List<NItemInfo> items)
        {
            this.Items.Clear();
            foreach (var info in items)
            {
                Item item = new Item(info);
                this.Items.Add(item.Id, item);
                Debug.LogFormat("ItemManager:Init[{0}]", item);
            }
            // 注册通知
            StatusService.Instance.RegisterStatusNofity(StatusType.Item, OnItemNotify);
        }

        private bool OnItemNotify(NStatus status)
        {
            if (status.Action == StatusAction.Add)
            {
                this.AddItem(status.Id, status.Value);
            }
            if (status.Action == StatusAction.Delete)
            {
                this.RemoveItem(status.Id, status.Value);
            }
            return true;
        }

        private void AddItem(int itemId, int count)
        {
            Item item = null;
            if (this.Items.TryGetValue(itemId, out item))
            {
                item.Count += count;
            }
            else
            {
                item = new Item(itemId, count);
                this.Items.Add(itemId, item);
            }
            BagManager.Instance.AddItem(itemId, count);
        }
        private void RemoveItem(int itemId, int count)
        {
            if(!this.Items.ContainsKey(itemId))
                return;
            Item item = this.Items[itemId];
            if (item.Count < count)
                return;
            item.Count -= count;
            BagManager.Instance.RemoveItem(itemId, count);

        }
    }
}

服务端代码

新增ItemService:

using Common;
using Common.Data;
using GameServer.Entities;
using GameServer.Managers;
using Network;
using SkillBridge.Message;

namespace GameServer.Services
{
    class ItemService : Singleton<ItemService>
    {
        public ItemService()
        {
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<ItemBuyRequest>(this.OnItemBuy);
        }
        public void Init()
        {

        }
        void OnItemBuy(NetConnection<NetSession> sender, ItemBuyRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("OnItemBuy: :character:{0} Shop:{1} ShopItem:{2}", character.Id, request.shopId, request.shopItemId);
            // 这里把信息保存到StatusManger了
            var result = ShopManager.Instance.BuyItem(sender, request.shopId, request.shopItemId);     
            sender.Session.Response.itemBuy = new ItemBuyResponse();
            sender.Session.Response.itemBuy.Result = result;
            // 看似发送一个,实际上自动打包StatusManager内容到StatusNotify信息中了,共三个信息
            sender.SendResponse();
        }
    }
}

新增ShopManager:

using GameServer.Entities;
using GameServer.Services;
using Network;
using SkillBridge.Message;

namespace GameServer.Managers
{
    class ShopManager : Singleton<ShopManager>
    {
        // 商店为所有人服务,单例类
        public Result BuyItem(NetConnection<NetSession> sender, int shopId ,int shopItemId)
        {
            if (!DataManager.Instance.Shops.ContainsKey(shopId))
                return Result.Failed;
            ShopItemDefine shopItem;
            if (DataManager.Instance.ShopItems[shopId].TryGetValue(shopItemId,out shopItem)) 
            {
                Log.InfoFormat("BuyItem: :character:{0} Item:{1} Count:{2} Price:{3}", sender.Session.Character.Id, shopItem.ItemID, shopItem.Count,shopItem.Price);
                if(sender.Session.Character.Gold >= shopItem.Price)
                {
                    sender.Session.Character.itemManager.AddItem(shopItem.ItemID, shopItem.Count);
                    sender.Session.Character.Gold -= shopItem.Price;
                    DBService.Instance.Save();
                    return Result.Success;
                }
            }
            return Result.Failed;
        }

    }
}

ItemManager两个方法都新增一行:

public bool AddItem(int itemId,int count)
{
    this.Owner.StatusManager.AddItemChange(itemId, count, StatusAction.Add);
}

public bool RemoveItem(int itemId, int count)
{
    this.Owner.StatusManager.AddItemChange(itemId, count, StatusAction.Delete);
}

Character新增的Gold属性:

public TCharacter Data;
public long Gold
{
    get { return this.Data.Gold; }
    set
    {
        if (this.Data.Gold == value)
            return;
        this.StatusManager.AddGoldChange((int)(value - this.Data.Gold));
        this.Data.Gold = value;
    }
}

装备系统

服务器使用指针需要允许使用Unsafe代码,在项目属性->生成中设置

数据添加

协议:

enum EQUIP_SLOT{
	WEAPON = 0;    // 武器
	ACCESSORY = 1; // 副手
	HELMET = 2;    // 头盔
	CHEST = 3;     // 胸甲
	SHOULDER = 4;  // 护肩
	PANTS = 5;     // 裤子
	BOOTS = 6;     // 靴子
	SLOT_MAX = 7;
}
message NetMessageRequest{
	ItemEquipRequest itemEquip = 11;
}
message NetMessageResponse{
	ItemEquipResponse itemEquip = 11;
}
message NCharacterInfo {
	bytes Equips = 12;
}
// 一个协议支撑穿脱
message ItemEquipRequest{
	int32 slot = 1;
	int32 itemId = 2;
	bool isEquip = 3;
}
message ItemEquipResponse{
	RESULT result = 1;
	string errormsg = 2;
}

同时要更改数据库中表TCharacter增加Equips字段。同时在Common.Data中新增配置表信息。

服务器代码

ItemService类新增代码

public ItemService()
{
    MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<ItemEquipRequest>(this.OnItemEquip);
}
void OnItemEquip(NetConnection<NetSession> sender, ItemEquipRequest request)
{
    Character character = sender.Session.Character;
    Log.InfoFormat("OnItemBuy: :character:{0} Slot:{1} Item:{2} Equip:{3}", character.Id, request.Slot,request.itemId ,request.isEquip.ToString());
    var result = EquipManager.Instance.EquipItem(sender, request.Slot, request.itemId,request.isEquip);  
    sender.Session.Response.itemEquip = new ItemEquipResponse();
    sender.Session.Response.itemEquip.Result = result;
    sender.SendResponse();                 
}

新增EquipManager类:

using Common;
using GameServer.Entities;
using GameServer.Services;
using Network;
using SkillBridge.Message;

namespace GameServer.Managers
{
    class EquipManager : Singleton<EquipManager>
    {
        public Result EquipItem(NetConnection<NetSession> sender, int slot, int itemId,bool isEquip)
        { 
            Character character = sender.Session.Character;
            if (!character.ItemManager.Items.ContainsKey(itemId))
                return Result.Failed;
            if(character.Define.Class != DataManager.Instance.Items[itemId].LimitClass)
                return Result.Failed;
            UpdateEquip(character.Data.Equips, slot, itemId, isEquip);
            DBService.Instance.Save();
            return Result.Success;
        }
        // 穿脱装备
        unsafe void UpdateEquip(byte[] equipData, int slot, int itemId, bool isEquip)
        {
            fixed(byte* pt = equipData)
            {
                int* slotid = (int*)(pt + slot * sizeof(int));   // 第slot个槽位的指针
                if (isEquip)
                    *slotid = itemId;                             // 穿装备
                else
                    *slotid = 0;                               // 脱装备
            }
        }


    }
}

客户端代码

UI层:UICharEquip

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Managers;
using Models;
using SkillBridge.Message;

class UICharEquip : UIWindow
{
    public Text title;
    public Text money;

    public GameObject itemPrefab;
    public GameObject itemEquipedPrefab;

    public Transform itemListRoot;

    public List<Transform> slots;

    private void Start()
    {
        RefreshUI();
        EquipManager.Instance.OnEquipChanged += RefreshUI;
    }

    private void OnDestroy()
    {
        // 一定要销毁,否则会和以前的事件叠加在一起
        EquipManager.Instance.OnEquipChanged -= RefreshUI;
    }
    // 只能选中一个Item
    UIEquipItem selectItem;
    public void SelectEquipItem(UIEquipItem uiEquipItem)
    {
        if (selectItem != null)
            selectItem.Selected = false;
        selectItem = uiEquipItem;
    }

    void RefreshUI()
    {
        ClearAllEquipList();
        InitAllEquipItems();
        ClearEquipedList();
        InitEquipedItems();
        this.money.text = User.Instance.CurrentCharacter.Gold.ToString();
    }

    void InitAllEquipItems()
    {
        foreach (var kv in ItemManager.Instance.Items)
        {
            // 不是本职业装备不显示
            if (kv.Value.Define.Type == ItemType.Equip && kv.Value.Define.LimitClass == User.Instance.CurrentCharacter.Class)
            {
                // 已经装备就不显示了
                if (EquipManager.Instance.Contains(kv.Key))
                    continue;
                GameObject go = Instantiate(itemPrefab, itemListRoot);
                UIEquipItem ui = go.GetComponent<UIEquipItem>();
                ui.SetEquipItem(kv.Key, kv.Value, this, false);
            }
        }
    }

    void ClearAllEquipList()
    {
        foreach (var item in itemListRoot.GetComponentsInChildren<UIEquipItem>())
        {
            Destroy(item.gameObject);
        }
    }

    void ClearEquipedList()
    {
        foreach (var item in slots)
        {
            if (item.childCount > 0)
                Destroy(item.GetChild(0).gameObject);
        }   
    }

    void InitEquipedItems()
    {
        for (int i = 0; i < (int)EquipSlot.SlotMax; i++)
        {
            var item = EquipManager.Instance.Equips[i];
            if (item != null)
            {
                GameObject go = Instantiate(itemEquipedPrefab, slots[i]);
                UIEquipItem ui = go.GetComponent<UIEquipItem>();
                ui.SetEquipItem(i, item, this, true);
            }
        }
    }

    // 穿脱是点击子元素调用的
    public void DoEquip(Item item)
    {
        EquipManager.Instance.EquipItem(item);
    }
    public void UnEquip(Item item)
    {
        EquipManager.Instance.UnEquipItem(item);
    }
}

UI层:UIEquipItem

using Common.Data;
using Managers;
using Models;
using SkillBridge.Message;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

class UIEquipItem : MonoBehaviour, IPointerClickHandler   // 这个接口处理鼠标点击事件
{
    public Image icon;
    public Text title;
    public Text level;
    public Text limitClass;
    public Text limitCategory;

    public Image background;
    public Sprite normalBg;
    public Sprite selectedBg;

    private bool selected;

    public bool Selected
    {
        get { return selected; }
        set
        {
            selected = value;
            this.background.overrideSprite = selected ? selectedBg : normalBg;
        }
    }
    public int index { get; set; }
    private UICharEquip owner;

    private Item item;
    void Start()
    {

    }

    bool isEquiped = false;
    public void SetEquipItem(int idx, Item item, UICharEquip owner, bool equiped)
    {
        this.owner = owner;
        this.index = idx;
        this.item = item;
        this.isEquiped = equiped;

        if (this.title != null) this.title.text = this.item.Define.Name;
        if (this.level != null) this.level.text = "Lv " + this.item.Define.Level.ToString();
        if (this.limitClass != null) this.limitClass.text = this.item.Define.LimitClass.ToString();
        if (this.limitCategory != null) this.limitCategory.text = this.item.Define.Category;
        if (this.icon != null) this.icon.overrideSprite = Resloader.Load<Sprite>(this.item.Define.Icon);
    }

    // 点击两次装备,点击一次脱下
    public void OnPointerClick(PointerEventData eventData)
    {
        if (this.isEquiped)
        {
            UnEquip();
        }
        else
        {
            if (this.selected)
            {
                DoEquip();
                this.selected = false;
            }
            else
            {
                this.Selected = true;
                this.owner.SelectEquipItem(this);
            }
        }
    }

    void DoEquip()
    {
        var msg = MessageBox.Show(string.Format("要装备[{0}]吗?", this.item.Define.Name), "确认", MessageBoxType.Confirm);
        msg.OnYes = () =>
        {
            var oldEquip = EquipManager.Instance.GetEquip(item.EquipInfo.Slot);
            if (oldEquip != null)
            {
                var newmsg = MessageBox.Show(string.Format("要替换[{0}]吗?", this.item.Define.Name), "确认", MessageBoxType.Confirm);
                newmsg.OnYes = () =>
                {
                    this.owner.DoEquip(this.item);
                };
            }
            else
                this.owner.DoEquip(this.item);
        };
    }

    void UnEquip()
    {
        var msg = MessageBox.Show(string.Format("要取下装备[{0}]吗?", this.item.Define.Name), "确认", MessageBoxType.Confirm);
        msg.OnYes = () =>               // 注意可以这样处理消息框点击事件
        {
            this.owner.UnEquip(this.item);
        };
    }

}

Manager层:EquipManager

using Common.Data;
using Network;
using SkillBridge.Message;
using UnityEngine;
using Models;
using Services;
using Managers;

namespace Managers
{
    class EquipManager : Singleton<EquipManager>
    {
        public delegate void OnEquipChangeHandler();
        public event OnEquipChangeHandler OnEquipChanged;
        public Item[] Equips = new Item[(int)EquipSlot.SlotMax];
        byte[] Data;

        unsafe public void Init(byte[] data)
        {
            this.Data = data;                 // 这里赋值了Character中的背包数据引用
            this.ParseEquipData(data);
        }

        // 检查是否装备某道具
        public bool Contains(int equipId)
        {
            for (int i = 0; i < this.Equips.Length; i++)
            {
                if (Equips[i] != null && Equips[i].Id == equipId)
                    return true;
            }
            return false;
        }
        // 检查某格子道具
        public Item GetEquip(EquipSlot slot)
        {
            return Equips[(int)slot];
        }
        // 解析字节数据,变成装备数组
        unsafe void ParseEquipData(byte[] data)
        {
            fixed(byte* pt = this.Data)
            {
                for (int i = 0; i < Equips.Length; i++)
                {
                    int itemId = *(int*)(pt + i * sizeof(int));
                    if (itemId > 0)
                        Equips[i] = ItemManager.Instance.Items[itemId];
                    else
                        Equips[i] = null;
                }
            }
        }
        // 打包数据到服务端
        unsafe public byte[] GetEquipData()
        {
            fixed(byte* pt = Data)
            {
                for (int i = 0; i < (int)EquipSlot.SlotMax; i++)
                {
                    int* itemid = (int*)(pt + i * sizeof(int));
                    if (Equips[i] == null)
                        *itemid = 0;
                    else
                        *itemid = Equips[i].Id;
                }
            }
            return this.Data;
        }

        // 发请求
        public void EquipItem(Item equip)
        {
            ItemService.Instance.SendEquipItem(equip, true);
        }

        public void UnEquipItem(Item equip)
        {
            ItemService.Instance.SendEquipItem(equip, false);
        }

        // 收答复
        public void OnEquipItem(Item equip)
        {
            // 如果穿着装备,则返回
            if (this.Equips[(int)equip.EquipInfo.Slot] != null && this.Equips[(int)equip.EquipInfo.Slot].Id == equip.Id)
                return;
            this.Equips[(int)equip.EquipInfo.Slot] = ItemManager.Instance.Items[equip.Id]; ;
            if (OnEquipChanged != null)
                OnEquipChanged();
        }
        public void OnUnEquipItem(EquipSlot slot)
        {
            if (this.Equips[(int)slot] != null)
            {
                this.Equips[(int)slot] = null;
                if (OnEquipChanged != null)
                    OnEquipChanged();
            }
        }

    }
}

任务系统

系统开发流程

数据添加

数据库

所有新加字段都为int类型,五个数据记录一条任务:ID、Target1、Target2、target3、Status。代表任务的ID,任务的四个进度(例如道具或击杀数量)。任务状态(进行中、已完成未提交、已提交、失败)。

协议

message NetMessageRequest{
	QuestListRequest questList = 12;
	QuestAcceptRequest questAccept = 13;
	QuestSubmitRequest questSubmit = 14;
}

message NetMessageResponse{
	QuestListResponse questList = 12;
	QuestAcceptResponse questAccept = 13;
	QuestSubmitResponse questSubmit = 14;
}
// Quest System
enum QUEST_STATUS{
	IN_PROGRESS = 0;
	COMPLETED = 1;
	FINISHED = 2;
	FAILED = 3;
}
enum QUEST_LIST_TYPE{
	ALL = 0;
	IN_PROGRESS = 1;
	FINISHED = 2;
}
// 一条任务有两个ID,考虑到同一个任务可能出现多次,第二个id是唯一ID
message NQuestInfo{
	int32 quest_id = 1;
	int32 quest_guid = 2;
	QUEST_STATUS status = 3;
	repeated int32 target = 4;
}
// 返回任务列表
message QuestListRequest{
	QUEST_LIST_TYPE listType = 1;
}
message QuestListResponse{
	RESULT result = 1;
	string errormsg = 2;
	repeated NQuestInfo quests = 3;
}
// 接受任务,返回单个任务信息
message QuestAcceptRequest{
	int32 quest_id = 1;
}
message QuestAcceptResponse{
	RESULT 	result = 1;
	string errormsg = 2;
	NQuestInfo quest = 3;
}
message QuestSubmitRequest{
	int32 quest_id = 1;
}
message QuestSubmitResponse{
	RESULT 	result = 1;
	string errormsg = 2;
	NQuestInfo quest = 3;
}
message QuestAbandonRequest{
	int32 quest_id = 1;
}
message QuestAbandonResponse{
	RESULT 	result = 1;
	string errormsg = 2;
}

服务器代码

QuestService:

using Common;
using SkillBridge.Message;
using GameServer.Entities;
using Network;

namespace GameServer.Services
{
    class QuestService : Singleton<QuestService>
    {
        public QuestService()
        {
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<QuestAcceptRequest>(this.OnQuestAccept);
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<QuestSubmitRequest>(this.OnQuestSubmit);
        }
        public void Init()
        {

        }

        private void OnQuestAccept(NetConnection<NetSession> sender, QuestAcceptRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("QuestAcceptRequest: :character:{0}:QuestId:{1}", character.Id, request.QuestId);
            sender.Session.Response.questAccept = new QuestAcceptResponse();
            Result result = character.QuestManager.AcceptQuest(sender, request.QuestId);
            sender.Session.Response.questAccept.Result = result;
            sender.SendResponse();
        }
        private void OnQuestSubmit(NetConnection<NetSession> sender, QuestSubmitRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("QuestSubmitRequest: :character:{0}:QuestId:{1}", character.Id, request.QuestId);
            sender.Session.Response.questSubmit = new QuestSubmitResponse();
            Result result = character.QuestManager.SubmitQuest(sender, request.QuestId);
            sender.Session.Response.questSubmit.Result = result;
            sender.SendResponse();
        }      
    }
}

QuestManager:

using System.Collections.Generic;
using System.Linq;
using Common.Data;
using Common;
using GameServer.Entities;
using GameServer.Services;
using Network;
using SkillBridge.Message;

namespace GameServer.Managers
{
    class QuestManager
    {
        Character Owner;
        public QuestManager(Character owner)
        {
            this.Owner = owner;
        }
        public void GetQuestInfos(List<NQuestInfo> list)
        {
            foreach (var quest in this.Owner.Data.Quests)
            {
                list.Add(GetQuestInfo(quest));
            }
        }
        public NQuestInfo GetQuestInfo(TCharacterQuest quest)
        {
            return new NQuestInfo()
            {
                QuestId = quest.QuestID,
                QuestGuid = quest.Id,
                Status = (QuestStatus)quest.Status,
                Targets = new int[3]
                {
                    quest.Target1,
                    quest.Target2,
                    quest.Target3,
                }
            };
        }

        public Result AcceptQuest(NetConnection<NetSession> sender,int questId)
        {
            Character character = sender.Session.Character;
            QuestDefine quest;
            // 数据库新建内容
            if (DataManager.Instance.Quests.TryGetValue(questId,out quest))
            {
                var dbquest = DBService.Instance.Entities.CharacterQuests.Create();
                dbquest.QuestID = quest.ID;
                // 没有目标直接完成
                if (quest.Target1 == QuestTarget.None)
                {
                    dbquest.Status = (int)QuestStatus.Completed;
                }
                else
                {
                    dbquest.Status = (int)QuestStatus.InProgress;
                }
                // db数据转换为网络数据
                sender.Session.Response.questAccept.Quest = this.GetQuestInfo(dbquest);
                character.Data.Quests.Add(dbquest);
                DBService.Instance.Save();
                return Result.Success;
            }
            else
            {
                sender.Session.Response.questAccept.Errormsg = "任务不存在";
                return Result.Failed;
            }
        }

        public Result SubmitQuest(NetConnection<NetSession> sender, int questId)
        {
            Character character = sender.Session.Character;
            QuestDefine quest;
            // 数据库新建内容
            if (DataManager.Instance.Quests.TryGetValue(questId, out quest))
            {
                var dbquest = character.Data.Quests.Where(q => q.QuestID == questId).FirstOrDefault();
                if (dbquest != null)
                {
                    if (dbquest.Status != (int)QuestStatus.Completed)
                    {
                        sender.Session.Response.questSubmit.Errormsg = "任务未完成";
                        return Result.Failed;
                    }
                    dbquest.Status = (int)QuestStatus.Finished;
                    sender.Session.Response.questSubmit.Quest = this.GetQuestInfo(dbquest);
                    DBService.Instance.Save();
                    // 处理任务奖励
                    if (quest.RewardGold > 0)
                    {
                        character.Gold += quest.RewardGold;
                    }
                    if (quest.RewardExp > 0)
                    {
                        //character.Exp += quest.RewardExp;
                    }
                    if (quest.RewardItem1 > 0)
                    {
                        character.ItemManager.AddItem(quest.RewardItem1, quest.RewardItem1Count);
                    }
                    if (quest.RewardItem2 > 0)
                    {
                        character.ItemManager.AddItem(quest.RewardItem2, quest.RewardItem2Count);
                    }
                    if (quest.RewardItem3 > 0)
                    {
                        character.ItemManager.AddItem(quest.RewardItem3, quest.RewardItem3Count);
                    }
                    DBService.Instance.Save();
                    sender.Session.Response.questSubmit.Errormsg = "任务完成";
                    return Result.Success;
                }            
                sender.Session.Response.questSubmit.Errormsg = "任务不存在[2]";
                return Result.Failed;
            }
            else
            {
                sender.Session.Response.questSubmit.Errormsg = "任务不存在[1]";
                return Result.Failed;
            }
        }
    }
}

客户端代码

QuestService:

using System;
using UnityEngine;
using Managers;
using Models;
using Network;
using SkillBridge.Message;

namespace Services
{
    class QuestService : Singleton<QuestService>, IDisposable
    {
        public QuestService()
        {
            MessageDistributer.Instance.Subscribe<QuestAcceptResponse>(this.OnQuestAccept);
            MessageDistributer.Instance.Subscribe<QuestSubmitResponse>(this.OnQuestSubmit);
        }
        public void Dispose()
        {
            MessageDistributer.Instance.Unsubscribe<QuestAcceptResponse>(this.OnQuestAccept);
            MessageDistributer.Instance.Unsubscribe<QuestSubmitResponse>(this.OnQuestSubmit);
        }
        public bool SendQuestAccept(Quest quest)
        {
            Debug.Log("SendQuestAccept");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.questAccept = new QuestAcceptRequest();
            message.Request.questAccept.QuestId = quest.Define.ID;
            NetClient.Instance.SendMessage(message);
            return true;
        }
        private void OnQuestAccept(object sender, QuestAcceptResponse message)
        {
            Debug.LogFormat("OnQuestAccept:{0},ERR:{1}", message.Result, message.Errormsg);
            if (message.Result == Result.Success)
            {
                QuestManager.Instance.OnQuestAccepted(message.Quest);
            }
            else
            {
                MessageBox.Show("任务接受失败", "错误",MessageBoxType.Error);
            }
        }
        public bool SendQuestSubmit(Quest quest)
        {
            Debug.Log("SendQuestSubmit");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.questSubmit = new QuestSubmitRequest();
            message.Request.questSubmit.QuestId = quest.Define.ID;
            NetClient.Instance.SendMessage(message);
            return true;
        }

        private void OnQuestSubmit(object sender, QuestSubmitResponse message)
        {
            Debug.LogFormat("OnQuestSubmit:{0},ERR:{1}", message.Result, message.Errormsg);
            if (message.Result == Result.Success)
            {
                QuestManager.Instance.OnQuestSubmited(message.Quest);
            }
            else
            {
                MessageBox.Show("任务完成失败", "错误", MessageBoxType.Error);
            }
        }

        

    }
}

QuestManager:

using System.Collections.Generic;
using System.Linq;
using Models;
using SkillBridge.Message;
using Services;
using UnityEngine.Events;

namespace Managers
{
    public enum NpcQuestStatus
    {
        None = 0,         // 无任务
        Complete,         // 拥有已完成可提交的任务
        Available,        // 拥有可接受任务
        Incomplete,       // 拥有未完成任务
    }
    // 这个类维护了两个字典,一个是所有已接未接任务的列表,第二是NPC对应哪些任务,任务的进度
    class QuestManager : Singleton<QuestManager>
    {
        public List<NQuestInfo> questInfos;
        public Dictionary<int, Quest> allQuests = new Dictionary<int, Quest>();
        public Dictionary<int, Dictionary<NpcQuestStatus, List<Quest>>> npcQuests = new Dictionary<int, Dictionary<NpcQuestStatus, List<Quest>>>();
        public UnityAction<Quest> onQuestStatusChange;

        public void Init(List<NQuestInfo> quests)
        {
            this.questInfos = quests;
            allQuests.Clear();
            this.npcQuests.Clear();
            InitQuests();
        }

        void InitQuests()
        {
            // 把已接任务添加到NPC身上,并加入列表
            foreach (var info in this.questInfos)
            {
                Quest quest = new Quest(info);
                this.allQuests[quest.Info.QuestId] = quest;
            }
            this.CheckAvailableQuests();
            foreach (var kv in this.allQuests)
            {
                this.AddNpcQuest(kv.Value.Define.AcceptNPC, kv.Value);
                this.AddNpcQuest(kv.Value.Define.SubmitNPC, kv.Value);
            }
        }
        // 可接任务加入列表
        void CheckAvailableQuests()
        {
            // 没接的任务
            foreach (var kv in DataManager.Instance.Quests)
            {
                // 如果已接任务,等级或职业不符合要求,则返回
                if (kv.Value.LimitClass != CharacterClass.None && kv.Value.LimitClass != User.Instance.CurrentCharacter.Class)
                    continue;
                if (kv.Value.LimitLevel > User.Instance.CurrentCharacter.Level)
                    continue;
                if (this.allQuests.ContainsKey(kv.Key))
                    continue;
                if (kv.Value.PreQuest > 0)
                {
                    Quest preQuest;
                    // 如果没接前置任务或没完成,则返回
                    if (this.allQuests.TryGetValue(kv.Value.PreQuest, out preQuest))
                    {
                        if (preQuest.Info == null)
                            continue;
                        if (preQuest.Info.Status != QuestStatus.Finished)
                            continue;
                    }
                    else
                        continue;
                }
                Quest quest = new Quest(kv.Value);
                this.allQuests[quest.Define.ID] = quest;
            }
        }


        void AddNpcQuest(int npcId,Quest quest)
        {
            if (!this.npcQuests.ContainsKey(npcId))
                this.npcQuests[npcId] = new Dictionary<NpcQuestStatus, List<Quest>>();
            List<Quest> availables;
            List<Quest> completes;
            List<Quest> incompletes;

            if (!this.npcQuests[npcId].TryGetValue(NpcQuestStatus.Available,out availables))
            {
                availables = new List<Quest>();
                this.npcQuests[npcId][NpcQuestStatus.Available] = availables;
            }
            if (!this.npcQuests[npcId].TryGetValue(NpcQuestStatus.Complete, out availables))
            {
                completes = new List<Quest>();
                this.npcQuests[npcId][NpcQuestStatus.Complete] = completes;
            }
            if (!this.npcQuests[npcId].TryGetValue(NpcQuestStatus.Incomplete, out availables))
            {
                incompletes = new List<Quest>();
                this.npcQuests[npcId][NpcQuestStatus.Incomplete] = incompletes;
            }
            if (quest.Info == null)  // 没有传进来网络信息(未接的)
            {
                if (npcId == quest.Define.AcceptNPC && !this.npcQuests[npcId][NpcQuestStatus.Available].Contains(quest))
                {
                    this.npcQuests[npcId][NpcQuestStatus.Available].Add(quest);
                }
            }
            else             // 已接任务
            {
                if (quest.Define.SubmitNPC == npcId && quest.Info.Status == QuestStatus.Completed)
                {
                    if (!npcQuests[npcId][NpcQuestStatus.Complete].Contains(quest))
                    {
                        this.npcQuests[npcId][NpcQuestStatus.Complete].Add(quest);
                    }
                }
                if (quest.Define.SubmitNPC == npcId && quest.Info.Status == QuestStatus.InProgress)
                {
                    if (!npcQuests[npcId][NpcQuestStatus.Incomplete].Contains(quest))
                    {
                        this.npcQuests[npcId][NpcQuestStatus.Incomplete].Add(quest);
                    }
                }
            }

        }
        // 获取NPC任务状态
        public NpcQuestStatus GetQuestStatusByNpc(int npcId)
        {
            Dictionary<NpcQuestStatus, List<Quest>> status = new Dictionary<NpcQuestStatus, List<Quest>>();
            if (this.npcQuests.TryGetValue(npcId,out status))
            {
                if (status[NpcQuestStatus.Complete].Count > 0)
                    return NpcQuestStatus.Complete;
                if (status[NpcQuestStatus.Available].Count > 0)
                    return NpcQuestStatus.Available;
                if (status[NpcQuestStatus.Incomplete].Count > 0)
                    return NpcQuestStatus.Incomplete;
            }
            return NpcQuestStatus.None;
        }
        // 获取NPC任务,展示对话框
        public bool OpenNpcQuest(int npcId)
        {
            Dictionary<NpcQuestStatus, List<Quest>> status = new Dictionary<NpcQuestStatus, List<Quest>>();
            if (this.npcQuests.TryGetValue(npcId,out status))
            {
                if (status[NpcQuestStatus.Complete].Count > 0)
                    return ShowQuestDialog(status[NpcQuestStatus.Complete].First());   // 展示第一个元素
                if (status[NpcQuestStatus.Available].Count > 0)
                    return ShowQuestDialog(status[NpcQuestStatus.Available].First());
                if (status[NpcQuestStatus.Incomplete].Count > 0)
                    return ShowQuestDialog(status[NpcQuestStatus.Incomplete].First());
            }
            return false;
        }


        bool ShowQuestDialog(Quest quest)
        {
            // 如果未接或已完成
            if (quest.Info == null || quest.Info.Status == QuestStatus.Completed)
            {
                UIQuestDialog dlg = UIManager.Instance.Show<UIQuestDialog>();
                dlg.SetQuest(quest);
                dlg.OnClose += OnQuestDialogClose;
                return true;
            }
            if (quest.Info != null || quest.Info.Status == QuestStatus.Completed)
            {
                if (!string.IsNullOrEmpty(quest.Define.DialogIncomplete))
                    MessageBox.Show(quest.Define.DialogIncomplete);
            }
            return true;
        }
        // 接受或拒绝任务的反应
        private void OnQuestDialogClose(UIWindow sender, UIWindow.WindowResult result)
        {
            UIQuestDialog dlg= (UIQuestDialog)sender;
            if (result == UIWindow.WindowResult.Yes)
            {
                if (dlg.quest.Info == null)
                    QuestService.Instance.SendQuestAccept(dlg.quest);
                else if (dlg.quest.Info.Status == QuestStatus.Completed)
                    QuestService.Instance.SendQuestSubmit(dlg.quest);
                
            }
            else if (result == UIWindow.WindowResult.No)
            {
                MessageBox.Show(dlg.quest.Define.DialogDeny);
            }
        }
        Quest RefreshQuestStatus(NQuestInfo quest)
        {
            this.npcQuests.Clear();
            Quest result;
            if (this.allQuests.ContainsKey(quest.QuestId))
            {
                this.allQuests[quest.QuestId].Info = quest;
                result = this.allQuests[quest.QuestId];
            }
            else
            {
                result = new Quest(quest);
                this.allQuests[quest.QuestId] = result;
            }
            foreach (var kv in this.allQuests)
            {
                this.AddNpcQuest(kv.Value.Define.AcceptNPC, kv.Value);
                this.AddNpcQuest(kv.Value.Define.SubmitNPC, kv.Value);
            }
            if (onQuestStatusChange != null)
                onQuestStatusChange(result);
            return result;
        }

        public void OnQuestAccepted(NQuestInfo info)
        {
            var quest = this.RefreshQuestStatus(info);
            MessageBox.Show(quest.Define.DialogAccept);
        }
        public void OnQuestSubmited(NQuestInfo info)
        {
            var quest = this.RefreshQuestStatus(info);
            MessageBox.Show(quest.Define.DialogFinish);
        }
    }
}

UI层:UIQuestDialog

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Models;
using SkillBridge.Message;

public class UIQuestDialog : UIWindow
{
    public UIQuestInfo questInfo;
    public Quest quest;
    public GameObject openButtons;
    public GameObject submitButtons;

    void Start()
    {
        
    }
    // 通过Quest刷新UI
    public void SetQuest(Quest quest)
    {
        this.quest = quest;
        this.UpdateQuest();
        // 看看是否没接的任务
        if (this.quest.Info == null)
        {
            openButtons.SetActive(true);
            submitButtons.SetActive(false);

        }
        else
        {
            if (this.quest.Info.Status == QuestStatus.Completed)
            {
                openButtons.SetActive(false);
                submitButtons.SetActive(true);
            }
            else
            {
                openButtons.SetActive(false);
                submitButtons.SetActive(false);
            }
        }
    }
    // 通过quest设定好questInfo内部的值,更新了UI
    void UpdateQuest()
    {
        if (this.quest != null)
        {
            if (this.questInfo != null)
            {
                if (this.questInfo != null)
                {
                    this.questInfo.SetQuestInfo(quest);
                }
            }
        }
    }
}

UI层:UIQuestInfo

using System;
using System.Collections.Generic;
using Models;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using SkillBridge.Message;

public class UIQuestInfo : MonoBehaviour
{
    public Text title;
    public Text[] targets;
    public Text description;
    public UIIconItem rewardItems;
    public Text rewardMoney;
    public Text rewardExp;

    public void SetQuestInfo(Quest quest)
    {
        this.title.text = string.Format("[{0}]{1}", quest.Define.Type, quest.Define.Name);
        if(quest.Info == null)
        {
            this.description.text = quest.Define.Dialog;
        }
        else
        {
            if (quest.Info.Status == QuestStatus.Completed)
            {
                this.description.text = quest.Define.DialogFinish;
            }
        }
        this.rewardMoney.text = quest.Define.RewardGold.ToString();
        this.rewardExp.text = quest.Define.RewardExp.ToString();
        // 强制刷新一下布局
        foreach (var fitter in this.GetComponentsInChildren<ContentSizeFitter>())
        {
            fitter.SetLayoutVertical();
        }
    }
    public void OnClickAbandon()
    {

    }
}

UI层:UIQuestSystem

using Common.Data;
using UnityEngine;
using UnityEngine.UI;
using Managers;

public class UIQuestSystem : UIWindow
{
    public Text title;
    public GameObject itemPrefab;
    public TabView Tabs;
    // 这两个ListView让类变得简单
    public ListView listMain;
    public ListView listBranch;

    public UIQuestInfo questInfo;
    private bool showAvailableList = false;

    void Start()
    {
        this.listMain.onItemSelected += this.OnQuestSelected;
        this.listBranch.onItemSelected += this.OnQuestSelected;
        this.Tabs.OnTabSelect += OnSelectTab;
        RefreshUI();
    }
    public void OnSelectTab(int idx)
    {
        showAvailableList = idx == 1;     // 1代表可接
        RefreshUI();
    }
    void RefreshUI()
    {
        ClearAllQuestList();
        InitAllQuestItems();
    }
    void ClearAllQuestList()
    {
        this.listMain.RemoveAll();
        this.listBranch.RemoveAll();
    }
    void InitAllQuestItems()
    {
        foreach (var kv in QuestManager.Instance.allQuests)
        {
            // 如果显示可接任务,那就不显示已接任务。如果不显示可接任务,就显示已接任务
            if (showAvailableList)
            {
                if (kv.Value.Info != null)
                    continue;
            }
            else
            {
                if (kv.Value.Info == null)
                    continue;
            }
            GameObject go = Instantiate(itemPrefab, kv.Value.Define.Type == QuestType.Main ? this.listMain.transform : this.listBranch.transform);
            UIQuestItem ui = go.GetComponent<UIQuestItem>();
            ui.SetQuestInfo(kv.Value);
            if (kv.Value.Define.Type == QuestType.Main)
                this.listMain.AddItem(ui);
            else
                this.listBranch.AddItem(ui);
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    private void OnQuestSelected(ListView.ListViewItem item)
    {
        UIQuestItem questItem = item as UIQuestItem;
        this.questInfo.SetQuestInfo(questItem.quest);
    }

    private void OnDestroy()
    {
        
    }
}

UI层:解决列表选中状态的脚本,往父节点添加ListView脚本,子物体继承ListView.ListViewItem,父物体AddItem即可,父物体中添加的所有子物体不会有重复的选中状态

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

public class ListView : MonoBehaviour
{
    public UnityAction<ListViewItem> onItemSelected;
    // 列表元素,被选中执行OnSelect方法
    public class ListViewItem : MonoBehaviour, IPointerClickHandler
    {
        private bool selected;
        public bool Selected
        {
            get { return selected; }
            set
            {
                selected = value;
                onSelected(selected);
            }
        }
        public virtual void onSelected(bool selected)
        {
        }

        public ListView owner;

        public void OnPointerClick(PointerEventData eventData)
        {
            if (!this.selected)
            {
                this.Selected = true;
            }
            if (owner != null && owner.SelectedItem != this)
            {
                owner.SelectedItem = this;
            }
        }
    }

    List<ListViewItem> items = new List<ListViewItem>();
    // 选中的元素唯一,当选中元素发生改变,执行onItemSelect方法
    private ListViewItem selectedItem = null;
    public ListViewItem SelectedItem
    {
        get { return selectedItem; }
        private set
        {
            if (selectedItem!=null && selectedItem != value)
            {
                selectedItem.Selected = false;
            }
            selectedItem = value;
            if (onItemSelected != null)
                onItemSelected.Invoke((ListViewItem)value);
        }
    }

    public void AddItem(ListViewItem item)
    {
        item.owner = this;
        this.items.Add(item);
    }

    public void RemoveAll()
    {
        foreach(var it in items)
        {
            Destroy(it.gameObject);
        }
        items.Clear();
    }
}

刷怪系统

需求分析

怪物和玩家一样,都是实体Entity,怪物进入地图的逻辑和玩家进入地图的逻辑其实是一样的。所以我们可以复用玩家进入地图的service。可以复用协议。

代码分析

服务器

Monster

using GameServer.Core;
using SkillBridge.Message;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GameServer.Entities
{
    class Monster : CharacterBase
    {
        public Monster(int tid, int level, Vector3Int pos, Vector3Int dir) : base(CharacterType.Monster, tid, level, pos, dir)
        {

        }
    }
}

MonsterManager:和CharacterManager的单例不同,每一个Map都有一个自己的MonsterManager

using System.Collections.Generic;
using SkillBridge.Message;
using GameServer.Entities;
using GameServer.Managers;
using GameServer.Models;

namespace GameServer.Managers
{
    class MonsterManager
    {
        private Map map;
        public Dictionary<int, Monster> Monsters = new Dictionary<int, Monster>();

        public void Init(Map map)
        {
            this.map = map;
        }
        // 这里调用的地方在Spawner中
        public Monster Create(int spawnMonID,int spawnLevel,NVector3 position,NVector3 direction)
        {
            // 生成一个怪物,并添加进列表管理
            Monster monster = new Monster(spawnMonID, spawnLevel, position, direction);
            EntityManager.Instance.AddEntity(this.map.ID,monster);
            monster.Info.Id = monster.entityId;            
            this.Monsters[monster.Id] = monster;
            // 发送信息给玩家
            this.map.MonsterEnter(monster);
            return monster;
        }

    }
}

Map:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SkillBridge.Message;

using Common;
using Common.Data;

using Network;
using GameServer.Managers;
using GameServer.Entities;
using GameServer.Services;

namespace GameServer.Models
{
    class Map
    {
        internal class MapCharacter
        {
            public NetConnection<NetSession> connection;
            public Character character;

            public MapCharacter(NetConnection<NetSession> conn, Character cha)
            {
                this.connection = conn;
                this.character = cha;
            }
        }

        public int ID
        {
            get { return this.Define.ID; }
        }
        internal MapDefine Define;

        Dictionary<int, MapCharacter> MapCharacters = new Dictionary<int, MapCharacter>();
        // 刷怪管理器
        SpawnManager SpawnManager = new SpawnManager();
        public MonsterManager MonsterManager = new MonsterManager();

        internal Map(MapDefine define)
        {
            this.Define = define;
            this.SpawnManager.Init(this);
            this.MonsterManager.Init(this);
        }
        // 注意这里的更新,刷怪的关键
        internal void Update()
        {
            SpawnManager.Update();
        }

        /// 角色进入地图
        internal void CharacterEnter(NetConnection<NetSession> conn, Character character)
        {
            Log.InfoFormat("CharacterEnter: Map:{0} characterId:{1}", this.Define.ID, character.Id);

            character.Info.mapId = this.ID;
            this.MapCharacters[character.Id] = new MapCharacter(conn, character);
            conn.Session.Response.mapCharacterEnter = new MapCharacterEnterResponse();
            conn.Session.Response.mapCharacterEnter.mapId = this.Define.ID
            foreach (var kv in this.MapCharacters)
            {
                // 获取其他玩家信息
                conn.Session.Response.mapCharacterEnter.Characters.Add(kv.Value.character.Info);
                // 广播角色信息给其他玩家
                if (kv.Value.character != character)
                    this.AddCharacterEnterMap(kv.Value.connection, character.Info);
            }
            // 获取所有怪物信息
            foreach (var kv in MonsterManager.Monsters)
            {
                conn.Session.Response.mapCharacterEnter.Characters.Add(kv.Value.Info);
            }
            conn.SendResponse();
        }

        void AddCharacterEnterMap(NetConnection<NetSession> conn, NCharacterInfo character)
        {
            if (conn.Session.Response.mapCharacterEnter == null)
            {
                conn.Session.Response.mapCharacterEnter = new MapCharacterEnterResponse();
                conn.Session.Response.mapCharacterEnter.mapId = this.Define.ID;
            }
            conn.Session.Response.mapCharacterEnter.Characters.Add(character);
            // 这里可以不发,减少服务器压力,因为我们的底层可以把消息一次性发出
            conn.SendResponse();
        }

        internal void CharacterLeave(Character cha)
        {
            Log.InfoFormat("CharacterLeave: Map:{0} characterId:{1}", this.Define.ID, cha.Id);
            // 广播信息
            foreach (var kv in this.MapCharacters)
            {
                this.SendCharacterLeaveMap(kv.Value.connection, cha);
            }
            this.MapCharacters.Remove(cha.Id);
        }

        void SendCharacterLeaveMap(NetConnection<NetSession> conn, Character character)
        {
            conn.Session.Response.mapCharacterLeave = new MapCharacterLeaveResponse();
            conn.Session.Response.mapCharacterLeave.characterId = character.Id;
            conn.SendResponse();
        }

        internal void UpdateEntity(NEntitySync entity)
        {
            // 把自己的位置更新到服务器,并发送信息给别人
            foreach (var kv in this.MapCharacters)
            {
                if(kv.Value.character.entityId == entity.Id)
                {
                    kv.Value.character.Position = entity.Entity.Position;
                    kv.Value.character.Direction = entity.Entity.Direction;
                    kv.Value.character.Speed = entity.Entity.Speed;
                }
                else
                {
                    MapService.Instance.SendEntityUpdate(kv.Value.connection, entity);
                }
            }
        }
        //怪物进入地图发送响应的信息给所有玩家
        internal void MonsterEnter(Monster monster)
        {
            Log.InfoFormat("MonsterEnter: Map:{0} monsterId:{1}", this.Define.ID, monster.Id);
            foreach (var kv in this.MapCharacters)
            {
                this.AddCharacterEnterMap(kv.Value.connection, monster.Info);
            }
        }
    }
}

执行刷怪的关键Spawner和SpawnManager

using GameServer.Models;
using System.Collections.Generic;

namespace GameServer.Managers
{
    class SpawnManager
    {
        // 刷怪器列表,存储了配置表中每一条刷怪规则的数据
        private List<Spawner> Rules = new List<Spawner>();
        private Map map;
        public void Init(Map map)
        {
            this.map = map;
            if (DataManager.Instance.SpawnRules.ContainsKey(map.Define.ID))
            {
                foreach (var define in DataManager.Instance.SpawnRules[map.Define.ID].Values)
                {
                    this.Rules.Add(new Spawner(define, this.map));
                }
            }
        }

        public void Update()
        {
            if (Rules.Count == 0)
                return;
            for (int i = 0; i < this.Rules.Count ; i++)
            {
                this.Rules[i].Update();
            }
        }
    }
}
using Common;
using Common.Data;
using GameServer.Models;

namespace GameServer.Managers
{
    class Spawner
    {
        public SpawnRuleDefine Define { get; set; }
        private Map map;
        private float unspawnTime = 0;
        private bool spawned = false;
        private SpawnPointDefine spawnPoint = null;

        public Spawner(SpawnRuleDefine define,Map map)
        {
            this.Define = define;
            this.map = map;

            if (DataManager.Instance.SpawnPoints.ContainsKey(this.map.ID))
            {
                if (DataManager.Instance.SpawnPoints[this.map.ID].ContainsKey(this.Define.SpawnPoint))
                {
                    spawnPoint = DataManager.Instance.SpawnPoints[this.map.ID][this.Define.SpawnPoint];
                }
                else
                {
                    Log.ErrorFormat("SpawnRule[{0}] SpawnPoint[{1}] not existed", this.Define.ID, this.Define.SpawnPoint);
                }
            }
        }
        public void Update()
        {
            if (this.CanSpawn())
            {
                this.Spawn();
            }
        }

        bool CanSpawn()
        {
            if (this.spawned)
                return false;
            if (this.unspawnTime + this.Define.SpawnPeriod > Time.time)
                return false;
            return true;
        }
        // 执行刷怪
        public void Spawn()
        {
            this.spawned = true;
            Log.InfoFormat("Map[{0}] Spawn[{1}] Mon[{2}] At Point[{3}]", this.Define.MapID,this.Define.ID,this.Define.SpawnMonID,this.Define.SpawnPoint);
            this.map.MonsterManager.Create(this.Define.SpawnMonID, this.Define.SpawnLevel, this.spawnPoint.Position, this.spawnPoint.Direction);
        }
    }
}

总结
由地图Map来维护一个SpawnManager,SpawnManager维护一个Spawner列表,这个列表读取配置表数据把怪刷出来,刷出来之后怪归MonsterManager管。
然后Map再从MonsterManager中采集数据传给进入地图的玩家,并在每一个Monster生成的时候传给所有玩家。

客户端

客户端没新加一行代码就实现了这个功能,归功于良好的框架设计。
涉及的代码如下:
MapService:

void OnMapCharacterEnter(object sender,MapCharacterEnterResponse response)
{
    Debug.LogFormat("OnMapCharacterEnter:MapID :{0} Count:{1}", response.mapId, response.Characters.Count);
    // 刷新一下角色信息,并把角色给角色管理器
    foreach(var cha in response.Characters)
    {
        if(User.Instance.CurrentCharacter == null || User.Instance.CurrentCharacter.Id == cha.Id) 
        {
            User.Instance.CurrentCharacter = cha;

        }
        // 这里生成了角色和怪物
        CharacterManager.Instance.AddCharacter(cha);
    }
    // 确认地图Id,正式进入地图s
    if (CurrentMapId != response.mapId)
    {
        this.EnterMap(response.mapId);   
        this.CurrentMapId = response.mapId;
    }
}

社交系统

好友系统

需求分析

流程图:

分析:

组成:

数据添加

协议

// friend System
message NetMessageRequest{
	// 好友系统中客户端要发请求也要发响应
	FriendAddRequest friendAddReq = 15;
	FriendAddResponse friendAddRes = 16;
	FriendListRequest friendList = 17;
	FriendRemoveRequest friendRemove = 18;
}
message NetMessageResponse{
	// 好友系统中服务器要发请求也要发响应
	FriendAddRequest friendAddReq = 15;
	FriendAddResponse friendAddRes = 16;
	FriendListResponse friendList = 17;
	FriendRemoveResponse friendRemove = 18;
}

message NFriendInfo{
	int32 id = 1; 
	NCharacterInfo friendInfo = 2;
	int32 status = 3;
}
message FriendAddRequest{
	int32 from_id = 1;
	string from_name = 2;
	int32 to_id = 3;
	string to_name = 4;
}
message FriendAddResponse{
	RESULT result = 1;
	string errormsg = 2;
	FriendAddRequest request = 3;  // 这条请求一直带着
}
message FriendListRequest{

}
message FriendListResponse{
	RESULT result = 1;
	string errormsg = 2;
	repeated NFriendInfo friends = 3;
}
message FriendRemoveRequest{
	int32 id = 1;
	int32 friendId = 2;
}
message FriendRemoveResponse{
	RESULT result = 1;
	string errormsg = 2;
	int32 id = 3;
}

数据库

底层改造:PostProcess

这部分只涉及服务器:
接口定义:

namespace Network
{
    public interface IPostResponser
    {
        void PostProcess(NetMessageResponse message);
    }
}

Character新增

class Character : CharacterBase,IPostResponser
{
    public void PostProcess(NetMessageResponse message)
    {
        this.FriendManager.PostProcess(message);
        if (this.StatusManager.HasStatus)
        {
            this.StatusManager.PostProcess(message);
        }
    }
}

NetSession相关代码

public IPostResponser PostResponser { get; set; }
// 获取所有的信息打包成字节数组,可以直接发送到客户端了
public byte[] GetResponse()
{
    if (response != null)
    {
        if (PostResponser != null)
            this.PostResponser.PostProcess(Response);

        byte[] data = PackageHandler.PackMessage(response);
        response = null;   // 会话一结束清空信息
        return data;
    }
    return null;
}

在UserService初始化:

void OnGameEnter(NetConnection<NetSession> sender, UserGameEnterRequest request)
{
    // 后处理器的赋值
    sender.Session.PostResponser = character;
}

代码实现

服务器

FriendService:

using SkillBridge.Message;
using Common;
using Common.Data;
using Network;
using GameServer.Entities;
using GameServer.Managers;
using System.Linq;

namespace GameServer.Services
{
    class FriendService : Singleton<FriendService>
    {

        public FriendService()
        {
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<FriendAddRequest>(this.OnFriendAddRequest);
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<FriendAddResponse>(this.OnFriendAddResponse);
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<FriendRemoveRequest>(this.OnFriendRemove);
        }

        public void Init()
        {

        }
        private void OnFriendAddRequest(NetConnection<NetSession> sender, FriendAddRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("OnFriendAddRequest: FromId:{0} FromName{1} ToID:{2} ToName:{3}", request.FromId, request.FromName, request.ToId, request.ToName);
            if (request.ToId == 0)
            {
                foreach (var cha in CharacterManager.Instance.Characters)  // 查找在线角色
                {
                    if (cha.Value.Data.Name == request.ToName)
                    {
                        request.ToId = cha.Key;
                        break;
                    }
                }
            }
            NetConnection<NetSession> friend = null;
            // 检查是否已经是好友
            if (request.ToId > 0)
            {
                if (character.FriendManager.GetFriendInfo(request.ToId) != null)
                {
                    sender.Session.Response.friendAddRes = new FriendAddResponse();
                    sender.Session.Response.friendAddRes.Result = Result.Failed;
                    sender.Session.Response.friendAddRes.Errormsg = "已经是好友了";
                    sender.SendResponse();
                    return;
                }
                // 查找好友的Session,在线的玩家都有
                friend = SessionManager.Instance.GetSession(request.ToId);
            }
            // 检查好友是否下线
            if (friend == null)
            {
                sender.Session.Response.friendAddRes = new FriendAddResponse();
                sender.Session.Response.friendAddRes.Result = Result.Failed;
                sender.Session.Response.friendAddRes.Errormsg = "玩家不存在或不在线";
                sender.SendResponse();
                return;
            }
            Log.InfoFormat("ForwardRequest: FromId:{0} FromName{1} ToID:{2} ToName:{3}", request.FromId, request.FromName, request.ToId, request.ToName);
            friend.Session.Response.friendAddReq = request;
            friend.SendResponse();
        }
        private void OnFriendAddResponse(NetConnection<NetSession> sender, FriendAddResponse response)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("OnFriendAddResponse: character:{0} Result:{1} FromID{2} ToID:{3}", character.Id, response.Result, response.Request.FromId, response.Request.ToId);
            //sender.Session.Response.friendAddRes = response;
            // 判断请求者是否还在线
            var requester = SessionManager.Instance.GetSession(response.Request.FromId);
            if (requester == null)
            {
                sender.Session.Response.friendAddRes.Result = Result.Failed;
                sender.Session.Response.friendAddRes.Errormsg = "请求者已下线";
                sender.SendResponse();
                return;
            }
            if (response.Result == Result.Success)
            {            
                character.FriendManager.AddFriend(requester.Session.Character);
                requester.Session.Character.FriendManager.AddFriend(character);
                DBService.Instance.Save();
                // 发给请求者
                requester.Session.Response.friendAddRes = response;
                //requester.Session.Response.friendAddRes.Result = Result.Success;
                //requester.Session.Response.friendAddRes.Errormsg = "添加好友成功";
                requester.SendResponse();
                // 发给同意者
                sender.Session.Response.friendAddRes = response;
                sender.Session.Response.friendAddRes.Result = Result.Success;
                sender.Session.Response.friendAddRes.Errormsg = response.Request.FromName + "成为您的好友";
                sender.SendResponse();

            }
            // 不同意
            else
            {
                requester.Session.Response.friendAddRes = response;
                requester.SendResponse();
            }
            
        }
        private void OnFriendRemove(NetConnection<NetSession> sender, FriendRemoveRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("OnFriendRemove: character:{0} FriendReletionID:{1}", character.Id, request.Id);
            sender.Session.Response.friendRemove = new FriendRemoveResponse();
            sender.Session.Response.friendRemove.Id = request.Id;
            // 删自己的好友
            if (character.FriendManager.RemoveFriendByID(request.Id))
            {
                sender.Session.Response.friendRemove.Result = Result.Success;
                // 删别人好友中的自己
                var friend = SessionManager.Instance.GetSession(request.friendId);
                // 好友在线
                if (friend != null)
                {
                    friend.Session.Character.FriendManager.RemoveFriendByFriendId(character.Id);
                }
                // 不在线
                else
                {
                    this.RemoveFriend(request.friendId, character.Id);
                }
            }
            else
                sender.Session.Response.friendRemove.Result = Result.Failed;
            DBService.Instance.Save();
           
            sender.SendResponse();

        }
        void RemoveFriend(int charId,int friendId)
        {
            var removeItem = DBService.Instance.Entities.CharacterFriends.FirstOrDefault(v => v.CharacterID == charId && v.FriendID == friendId);
            if (removeItem != null)
            {
                DBService.Instance.Entities.CharacterFriends.Remove(removeItem);
            }
        }
    }
}

FriendManager:

using Common;
using GameServer.Entities;
using GameServer.Models;
using GameServer.Services;
using SkillBridge.Message;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GameServer.Managers
{
    class FriendManager
    {
        Character Owner;
        // 好友列表
        List<NFriendInfo> friends = new List<NFriendInfo>();
        bool friendChanged = false;

        public FriendManager(Character owner)
        {
            this.Owner = owner;
            this.InitFriends();
        }
        // 获取friends的复制列表
        public void GetFriendInfos(List<NFriendInfo> list)
        {
            foreach (var f in this.friends)
            {
                list.Add(f);
            }
        }
        // 在线获取朋友实时信息,不在线获取数据库信息
        public NFriendInfo GetFriendInfo(TCharacterFriend friend)
        {
            NFriendInfo friendInfo = new NFriendInfo();
            var character = CharacterManager.Instance.GetCharacter(friend.FriendID);
            friendInfo.friendInfo = new NCharacterInfo();
            friendInfo.Id = friend.Id;
            // 不在线
            if (character == null)
            {
                friendInfo.friendInfo.Id = friend.FriendID;
                friendInfo.friendInfo.Name = friend.FriendName;
                friendInfo.friendInfo.Class = (CharacterClass)friend.Class;
                friendInfo.friendInfo.Level = friend.Level;
                friendInfo.Status = 0;
            }
            else
            {
                friendInfo.friendInfo = GetBasicInfo(character.Info);
                friendInfo.friendInfo.Name = character.Info.Name;
                friendInfo.friendInfo.Class = character.Info.Class;
                friendInfo.friendInfo.Level = character.Info.Level;
                character.FriendManager.UpdateFriendInfo(this.Owner.Info, 1);
                friendInfo.Status = 1;
            }
            return friendInfo;
        }
        // 从数据库和实时数据中中更新Friends
        public void InitFriends()
        {
            this.friends.Clear();
            foreach (var friend in this.Owner.Data.Friends)
            {
                this.friends.Add(GetFriendInfo(friend));
            }
        }
        // 添加朋友到数据库中
        public void AddFriend(Character friend)
        {
            TCharacterFriend tf = new TCharacterFriend()
            {
                FriendID = friend.Id,
                FriendName = friend.Data.Name,
                Class = friend.Data.Class,
                Level = friend.Data.Level,
             };
            this.Owner.Data.Friends.Add(tf);
            friendChanged = true;
        }
        // 通过角色Id查找并删除朋友
        public bool RemoveFriendByFriendId(int friendid)
        {
            var removeItem = this.Owner.Data.Friends.FirstOrDefault(v => v.FriendID == friendid);
            if (removeItem != null)
            {
                DBService.Instance.Entities.CharacterFriends.Remove(removeItem);
                friendChanged = true;
                return true;
            }
            else
                return false;
        }
        // 通过数据库的朋友条目的唯一ID删除朋友
        public bool RemoveFriendByID(int id)
        {
            var removeItem = this.Owner.Data.Friends.FirstOrDefault(v => v.Id == id);
            if (removeItem != null)
            {
                DBService.Instance.Entities.CharacterFriends.Remove(removeItem);
                friendChanged = true;
                return true;
            }
            else
                return false;
        }
        // new一个新的信息
        NCharacterInfo GetBasicInfo(NCharacterInfo info)
        {
            return new NCharacterInfo()
            {
                Id = info.Id,
                Name = info.Name,
                Class = info.Class,
                Level = info.Level
            };
        }
        // 通过角色id查找朋友信息
        public NFriendInfo GetFriendInfo(int friendId)
        {
            foreach (var f in this.friends)
            {
                if (f.friendInfo.Id == friendId)
                {
                    return f; ;
                }
            }
            return null;
        }

        // 更新好友在线状态
        public void UpdateFriendInfo(NCharacterInfo friendInfo,int status)
        {
            foreach (var f in friends)
            {
                if (f.friendInfo.Id == friendInfo.Id)
                {
                    f.Status = status;
                    break;
                }
            }
            this.friendChanged = true;
        }
        // 发送信息
        public void PostProcess(NetMessageResponse message)
        {
            if (friendChanged)
            {
                // 从数据库更新好友
                this.InitFriends();
                // 发送好友列表
                if (message.friendList == null)
                {
                    message.friendList = new FriendListResponse();
                    message.friendList.Friends.AddRange(this.friends);
                }
                friendChanged = false;
            }
        }
    }
}

客户端

Service层:FriendService

using System;
using UnityEngine;
using Managers;
using Models;
using Network;
using SkillBridge.Message;
using UnityEngine.Events;

namespace Services
{
    class FriendService : Singleton<FriendService>
    {
        public UnityAction OnFriendUpdate;
        public void Init()
        {

        }
        public FriendService()
        {
            // 这里有一个请求,因为别人加你为好友时会收到这个
            MessageDistributer.Instance.Subscribe<FriendAddRequest>(this.OnFriendAddRequest);
            MessageDistributer.Instance.Subscribe<FriendAddResponse>(this.OnFriendAddResponse);
            MessageDistributer.Instance.Subscribe<FriendListResponse>(this.OnFriendList);
            MessageDistributer.Instance.Subscribe<FriendRemoveResponse>(this.OnFriendRemove);

        }
        public void Dispose()
        {
            MessageDistributer.Instance.Unsubscribe<FriendAddRequest>(this.OnFriendAddRequest);
            MessageDistributer.Instance.Unsubscribe<FriendAddResponse>(this.OnFriendAddResponse);
            MessageDistributer.Instance.Unsubscribe<FriendListResponse>(this.OnFriendList);
            MessageDistributer.Instance.Unsubscribe<FriendRemoveResponse>(this.OnFriendRemove);
        }
        public void SendFriendAddRequest(int friendId,string friendName)
        {
            Debug.Log("SendFriendAddRequest");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.friendAddReq = new FriendAddRequest();
            message.Request.friendAddReq.FromId = User.Instance.CurrentCharacter.Id;
            message.Request.friendAddReq.FromName = User.Instance.CurrentCharacter.Name;
            message.Request.friendAddReq.ToId = friendId;
            message.Request.friendAddReq.ToName = friendName;
            NetClient.Instance.SendMessage(message);
        }
        public void SendFriendAddResponse(bool accept, FriendAddRequest request)
        {
            Debug.Log("SendFriendAddResponse");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.friendAddRes = new FriendAddResponse();
            message.Request.friendAddRes.Result = accept ? Result.Success : Result.Failed;
            message.Request.friendAddRes.Errormsg = accept ? "对方同意了您的申请" : "对方拒绝了你的申请";
            message.Request.friendAddRes.Request = request;
            NetClient.Instance.SendMessage(message);
        }

        private void OnFriendAddRequest(object sender, FriendAddRequest request)
        {
            var confirm = MessageBox.Show(string.Format("{0}请求加你为好友", request.FromName), "好友请求", MessageBoxType.Confirm, "接受", "拒绝");
            confirm.OnYes = () => { this.SendFriendAddResponse(true, request); };
            confirm.OnNo = () => { this.SendFriendAddResponse(false, request); };
        }

        private void OnFriendAddResponse(object sender, FriendAddResponse message)
        {
            if (message.Result == Result.Success)
            {
                MessageBox.Show(message.Errormsg, "添加好友成功");
            }
            else
            {
                MessageBox.Show(message.Errormsg,"添加好友失败");
            }
        }
        // 接收好友列表
        private void OnFriendList(object sender, FriendListResponse message)
        {
            Debug.Log("OnFriendList");
            FriendManager.Instance.allFriends = message.Friends;
            if (this.OnFriendUpdate != null)
                this.OnFriendUpdate();
        }

        public void SendFriendRemoveRequest(int id,int friendId)
        {
            Debug.Log("SendFriendRemoveRequest");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.friendRemove = new FriendRemoveRequest();
            message.Request.friendRemove.Id = id;
            message.Request.friendRemove.friendId = friendId;
            NetClient.Instance.SendMessage(message);
        }

        private void OnFriendRemove(object sender, FriendRemoveResponse message)
        {
            if (message.Result == Result.Success)
                MessageBox.Show("删除成功", "删除好友");
            else        
                MessageBox.Show("删除失败", "删除好友", MessageBoxType.Error);
        }

    }
}

Manager层:FriendManager

using System.Collections.Generic;
using SkillBridge.Message;

namespace Managers
{
    class FriendManager : Singleton<FriendManager>
    {
        public List<NFriendInfo> allFriends;

        public void Init(List<NFriendInfo> friends)
        {
            this.allFriends = friends;
        }
    }
}

UI层:UIInputBox和InputBox(万能用途)

using UnityEngine;
class InputBox
{
    static Object cacheObject = null;

    public static UIInputBox Show(string message, string title = "",string btnOK = "", string btnCancel = "",string emptyTips = "")
    {
        if (cacheObject == null)
        {
            cacheObject = Resloader.Load<Object>("UI/UIInputBox");
        }

        GameObject go = (GameObject)GameObject.Instantiate(cacheObject);
        UIInputBox inputBox = go.GetComponent<UIInputBox>();
        inputBox.Init(title, message, btnOK, btnCancel, emptyTips);
        return inputBox;
    }
}
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class UIInputBox : MonoBehaviour
{
    public Text title;
    public Text message;
    public Text tips;
    public Button buttonYes;
    public Button buttonNo;
    public InputField input;

    public Text buttonYesTitle;
    public Text buttonNoTitle;

    public delegate bool SubmitHandler(string inputText, out string tips);
    public event SubmitHandler OnSubmit;
    public UnityAction OnCancel;
    public string emptyTips;

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }

    public void Init(string title, string message, string btnOK = "", string btnCancel = "",string emptyTips = "")
    {
        if (!string.IsNullOrEmpty(title)) this.title.text = title;
        this.message.text = message;
        this.tips.text = null;
        this.OnSubmit = null;
        this.emptyTips = emptyTips;

        if (!string.IsNullOrEmpty(btnOK)) this.buttonYesTitle.text = title;
        if (!string.IsNullOrEmpty(btnCancel)) this.buttonNoTitle.text = title;

        this.buttonYes.onClick.AddListener(OnClickYes);
        this.buttonNo.onClick.AddListener(OnClickNo);
    }

    void OnClickYes()
    {
        this.tips.text = "";
        // 没有输入就显示提示
        if (string.IsNullOrEmpty(input.text))
        {
            this.tips.text = this.emptyTips;
            return;
        }
        // 提交的时候返回提示
        if (OnSubmit != null)
        {
            string tips;
            if (!OnSubmit(this.input.text,out tips))
            {
                this.tips.text = tips;
                return;
            }
        }
        Destroy(this.gameObject);
    }

    void OnClickNo()
    {
        Destroy(this.gameObject);
        if (this.OnCancel != null)
            this.OnCancel();
    }
}

UI层:UIFriendItem和UIFriends,依旧是ListView发挥的时候了

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Common.Data;
using Managers;
using Models;
using SkillBridge.Message;
using UnityEngine.UI;

public class UIFriendItem : ListView.ListViewItem
{
    public Text nickname;
    public Text @class;
    public Text level;
    public Text status;

    public Image background;
    public Sprite normalBg;
    public Sprite selectedBg;

    public override void onSelected(bool selected)
    {
        this.background.overrideSprite = selected ? selectedBg : normalBg;
    }

    public NFriendInfo Info;

    public void SetFriendInfo(NFriendInfo item)
    {
        this.Info = item;
        if (this.nickname != null) this.nickname.text = this.Info.friendInfo.Name;
        if (this.@class != null) this.@class.text = this.Info.friendInfo.Class.ToString();
        if (this.level != null) this.level.text = this.Info.friendInfo.Level.ToString();
        if (this.status != null) this.status.text = this.Info.Status == 1 ? "在线" : "离线";
    }
}
using Managers;
using Models;
using Services;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIFriends : UIWindow
{
    public GameObject itemPrefab;
    public ListView listMain;
    public Transform itemRoot;
    public UIFriendItem selectedItem;

    void Start()
    {
        FriendService.Instance.OnFriendUpdate = RefreshUI;
        this.listMain.onItemSelected += this.OnFriendSelected;
        RefreshUI();
    }
    // 这个方法确定了选择那个元素
    public void OnFriendSelected(ListView.ListViewItem item)
    {
        this.selectedItem = item as UIFriendItem;
    }

    public void OnClickFriendAdd()
    {
        UIInputBox inputbox = InputBox.Show("输入要添加的好友名称和ID", "添加好友");
        inputbox.OnSubmit += OnFriendAddSubmit;
    }

    private bool OnFriendAddSubmit(string input,out string tips)
    {
        tips = "";
        int friendId = 0;
        string friendName = "";
        if (!int.TryParse(input, out friendId))
            friendName = input;
        if (friendId == User.Instance.CurrentCharacter.Id || friendName == User.Instance.CurrentCharacter.Name)
        {
            tips = "开玩笑嘛?不能添加自己哦";
            return false;
        }
        FriendService.Instance.SendFriendAddRequest(friendId, friendName);
        return true;
    }
    public void OnClickFriendChat()
    {
        MessageBox.Show("暂未开放");
    }
    public void OnClickFriendRemove()
    {
        if (selectedItem == null)
        {
            MessageBox.Show("请选择要删除的好友");
            return;
        }
        UIMessageBox uIMessageBox = MessageBox.Show(string.Format("确定要删除好友{0}吗?", selectedItem.Info.friendInfo.Name), "删除好友", MessageBoxType.Confirm, "删除","取消");
        uIMessageBox.OnYes = () =>{FriendService.Instance.SendFriendRemoveRequest(this.selectedItem.Info.Id, this.selectedItem.Info.friendInfo.Id);};
    }

    void RefreshUI()
    {
        ClearFriendList();
        InitFriendItems();
    }

    void ClearFriendList()
    {
        this.listMain.RemoveAll();
    }
    void InitFriendItems()
    {
        foreach (var item in FriendManager.Instance.allFriends)
        {
            GameObject go = Instantiate(itemPrefab, this.listMain.transform);
            UIFriendItem ui = go.GetComponent<UIFriendItem>();
            ui.SetFriendInfo(item);
            this.listMain.AddItem(ui);
        }
    }
}

ListView详解

用途:主要用于一个窗口摆放多个Item,且只能选中一个的时候。
用法:往父节点添加ListView脚本,子物体继承ListView.ListViewItem,父物体AddItem即可,父物体中添加的所有子物体不会有重复的选中状态
外部调用时,可以订阅其中的onItemSelected事件,每当被选中的时候触发这个事件。这个事件附带一个ListViewItem参数。
示范:

public ListView listMain;
this.listMain.onItemSelected += this.OnFriendSelected;

public void OnFriendSelected(ListView.ListViewItem item)
{
    this.selectedItem = item as UIFriendItem;
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

public class ListView : MonoBehaviour
{
    public UnityAction<ListViewItem> onItemSelected;
    // 列表元素,被选中执行OnSelect方法
    public class ListViewItem : MonoBehaviour, IPointerClickHandler
    {
        private bool selected;
        public bool Selected
        {
            get { return selected; }
            set
            {
                selected = value;
                onSelected(selected);
            }
        }
        public virtual void onSelected(bool selected)
        {
        }

        public ListView owner;

        public void OnPointerClick(PointerEventData eventData)
        {
            if (!this.selected)
            {
                this.Selected = true;
            }
            if (owner != null && owner.SelectedItem != this)
            {
                owner.SelectedItem = this;
            }
        }
    }

    List<ListViewItem> items = new List<ListViewItem>();
    // 选中的元素唯一,当选中元素发生改变,执行onItemSelect方法
    private ListViewItem selectedItem = null;
    public ListViewItem SelectedItem
    {
        get { return selectedItem; }
        private set
        {
            if (selectedItem!=null && selectedItem != value)
            {
                selectedItem.Selected = false;
            }
            selectedItem = value;
            if (onItemSelected != null)
                onItemSelected.Invoke((ListViewItem)value);
        }
    }

    public void AddItem(ListViewItem item)
    {
        item.owner = this;
        this.items.Add(item);
    }

    public void RemoveAll()
    {
        foreach(var it in items)
        {
            if(it != null)
                Destroy(it.gameObject);
        }
        items.Clear();
    }
}

组队系统

需求分析

流程图:

分析:

VS生成事件

为了简化我们的工作流程,不用在Common生成解决方案后,每次都要把dll文件复制给客户端。我们右键点击解决方案中的Common->属性->生成事件->后期生成事件命令行

copy $(TargetDir)Common.* $(ProjectDir)..\..\Client\Assets\References\ /Y
copy $(TargetDir)Protocol.* $(ProjectDir)..\..\Client\Assets\References\ /Y

就能在编译之后自动复制文件。

数据结构

协议

message NetMessageRequest{
	TeamInviteRequest teamInviteReq = 19;
	TeamInviteResponse teamInviteRes = 20;
	TeamInfoRequest teamInfo = 21;
	TeamLeaveRequest teamLeave = 22;
}

message NetMessageResponse{
	TeamInviteRequest teamInviteReq = 19;
	TeamInviteResponse teamInviteRes = 20;
	TeamInfoResponse teamInfo = 21;
	TeamLeaveResponse teamLeave = 22;
}

// Team System
message NTeamInfo{
	int32 id = 1; 
	int32 Leader = 2;
	repeated NCharacterInfo members = 3;
}
message TeamInviteRequest{
	int32 team_id = 1;
	int32 from_id = 2;
	string from_name = 3;
	int32 to_id = 4;
	string to_name = 5;
}
message TeamInviteResponse{
	RESULT result = 1;
	string errormsg = 2;
	TeamInviteRequest request = 3; 
}
message TeamInfoRequest{
}
message TeamInfoResponse{
	RESULT result = 1;
	string errormsg = 2;
	NTeamInfo team = 3;
}
message TeamLeaveRequest{
	int32 team_id = 1;
	int32 characterId = 2;
}
message TeamLeaveResponse{
	RESULT result = 1;
	string errormsg = 2;
	int32 characterId = 3;
}

代码实现

服务器

TeamService

using SkillBridge.Message;
using Common;
using Common.Data;
using Network;
using GameServer.Entities;
using GameServer.Managers;
using System.Linq;
using GameServer.Models;

namespace GameServer.Services
{
    class TeamService : Singleton<TeamService>
    {

        public TeamService()
        {
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<TeamInviteRequest>(this.OnTeamInviteRequest);
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<TeamInviteResponse>(this.OnTeamInviteResponse);
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<TeamLeaveRequest>(this.OnTeamLeave);
        }

        public void Init()
        {
            TeamManager.Instance.Init();
        }
        private void OnTeamInviteRequest(NetConnection<NetSession> sender, TeamInviteRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("OnTeamInviteRequest: FromId:{0} FromName{1} ToID:{2} ToName:{3}", request.FromId, request.FromName, request.ToId, request.ToName);
   
            NetConnection<NetSession> target = SessionManager.Instance.GetSession(request.ToId);;
            // 检查对方是否下线
            if (target == null)
            {
                sender.Session.Response.teamInviteRes = new TeamInviteResponse();
                sender.Session.Response.teamInviteRes.Result = Result.Failed;
                sender.Session.Response.teamInviteRes.Errormsg = "玩家不存在或不在线";
                sender.SendResponse();
                return;
            }
            if (target.Session.Character.Team != null)
            {
                sender.Session.Response.teamInviteRes = new TeamInviteResponse();
                sender.Session.Response.teamInviteRes.Result = Result.Failed;
                if(sender.Session.Character.Team == null || sender.Session.Character.Team.Id != target.Session.Character.Team.Id)
                    sender.Session.Response.teamInviteRes.Errormsg = "对方已经有队伍";
                else
                    sender.Session.Response.teamInviteRes.Errormsg = "对方已经在本队伍中";
                sender.SendResponse();
                return;
            }
            Log.InfoFormat("ForwardTeamInviteRequest: FromId:{0} FromName{1} ToID:{2} ToName:{3}", request.FromId, request.FromName, request.ToId, request.ToName);
            target.Session.Response.teamInviteReq = request;
            target.SendResponse();
        }
        private void OnTeamInviteResponse(NetConnection<NetSession> sender, TeamInviteResponse response)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("OnFriendAddResponse: character:{0} Result:{1} FromID{2} ToID:{3}", character.Id, response.Result, response.Request.FromId, response.Request.ToId);
            // 判断请求者是否还在线
            var requester = SessionManager.Instance.GetSession(response.Request.FromId);
            if (requester == null)
            {
                sender.Session.Response.friendAddRes.Result = Result.Failed;
                sender.Session.Response.friendAddRes.Errormsg = "请求者已下线";
                sender.SendResponse();
                return;
            }
            if (response.Result == Result.Success)
            {
                TeamManager.Instance.AddTeamMember(requester.Session.Character, character);
                // 发给请求者
                requester.Session.Response.teamInviteRes = response;
                requester.SendResponse();
                // 发给同意者
                sender.Session.Response.teamInviteRes = response;
                sender.Session.Response.teamInviteRes.Result = Result.Success;
                sender.Session.Response.teamInviteRes.Errormsg = string.Format("您加入了{0}的队伍",response.Request.FromName); 
                sender.SendResponse();

            }
            // 不同意
            else
            {
                requester.Session.Response.teamInviteRes = response;
                requester.SendResponse();
            }
            
        }
        // 发送队伍信息给客户端,自己加的用于获取进入游戏时的队伍
        public void SendTeamInfo(NetConnection<NetSession> sender)
        {         
            sender.Session.Response.teamInfo = new TeamInfoResponse();
            sender.Session.Response.teamInfo.Result = Result.Success;
            Team team = TeamManager.Instance.GetTeamByCharacter(sender.Session.Character.Id);
            if (team == null)
                sender.Session.Response.teamInfo.Team = null;
            else
            {
                sender.Session.Response.teamInfo.Team = new NTeamInfo();
                sender.Session.Response.teamInfo.Team.Id = team.Id;
                sender.Session.Response.teamInfo.Team.Leader = team.Leader.Id;
                foreach (var member in team.Members)
                {
                    sender.Session.Response.teamInfo.Team.Members.Add(member.GetBasicInfo());
                }
            }           
            sender.SendResponse();
        }

        private void OnTeamLeave(NetConnection<NetSession> sender, TeamLeaveRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("OnTeamLeave: character:{0} TeamId:{1} LeaveCharaceter:{2}", character.Id, request.TeamId, request.characterId);
            sender.Session.Response.teamLeave = new TeamLeaveResponse();
            sender.Session.Response.teamLeave.characterId = request.characterId;
            if (character.Team == null)
            {
                sender.Session.Response.teamLeave.Result = Result.Failed;
                sender.Session.Response.teamLeave.Errormsg = "您还未进入队伍";
            }
            else
            { 
                sender.Session.Response.teamLeave.Result = Result.Success;
                sender.Session.Response.teamLeave.Errormsg = "退出队伍成功";

                TeamManager.Instance.LeaveTeam(character);

            }
            sender.SendResponse();

        }

    }
}

TeamManager:

using Common;
using GameServer.Entities;
using GameServer.Models;
using GameServer.Services;
using SkillBridge.Message;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GameServer.Managers
{
    class TeamManager : Singleton<TeamManager>
    {
        // 列表为了遍历,字典为了精准查询,但是不会浪费一倍的空间,因为存放的都是引用
        public List<Team> Teams = new List<Team>();
        public Dictionary<int, Team> CharacterTeams = new Dictionary<int, Team>();  // 好像没用到

        public void Init()
        {

        }
        // 根据Id来查找Team
        public Team GetTeamByCharacter(int characterId)
        {
            Team team = null;
            this.CharacterTeams.TryGetValue(characterId, out team);
            return team;
        }
        // 添加队伍成员,如果没有队伍,就创建队伍(给上层调用)
        public void AddTeamMember(Character leader, Character member)
        {
            if (leader.Team == null)
            {
                leader.Team = CreateTeam(leader);
                CharacterTeams[leader.Id] = leader.Team;
            }
            leader.Team.AddMember(member);
            CharacterTeams[member.Id] = leader.Team;
        }

        public void RemoveTeamMember(Character leader,Character member)
        {

        }

        public void LeaveTeam(Character member)
        {
            if (member.Team == null) return;
            member.Team.Leave(member);
            CharacterTeams.Remove(member.Id);
        }

        // 这里的机制是为了应对玩家们的频繁创建解散队伍,一旦创建就不会销毁,除非关掉服务器,优化了对内存的使用
        Team CreateTeam(Character leader)
        {
            Team team = null;
            // 如果找到一个空队伍,就不用重新创建了
            for (int i = 0; i < this.Teams.Count; i++)
            {
                team = this.Teams[i];
                if (team.Members.Count == 0)
                {
                    team.AddMember(leader);
                    return team;
                }
            }
            team = new Team(leader);
            this.Teams.Add(team);
            team.Id = this.Teams.Count;
            return team;
        }

    }
}

Team:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common;
using GameServer.Entities;
using SkillBridge.Message;

namespace GameServer.Models
{
    class Team
    {
        public int Id;
        public Character Leader;
        public List<Character> Members = new List<Character>();
        // 为了队伍的每一个人能收到消息,我们需要加时间戳,代表队伍变更的时间
        public int timestamp;
        public Team(Character leader)
        {
            AddMember(leader);
        }
        public void AddMember(Character member)
        {
            if (this.Members.Count == 0)
            {
                this.Leader = member;
            }
            this.Members.Add(member);
            member.Team = this;
            timestamp = Time.timestamp;
        }

        public void Leave(Character member)
        {
            Log.InfoFormat("Leave Team: {0}:{1}", member.Id, member.Info.Name);
            foreach (var item in Members)
            {
                if (member.Id == item.Id)
                {
                    this.Members.Remove(item);
                    break;
                }
            }
            if (member == this.Leader)
            {
                if (this.Members.Count > 0)
                    this.Leader = this.Members[0];
                else
                    this.Leader = null;
            }
            member.Team = null;
            timestamp = Time.timestamp;
        }

        public void PostProcess(NetMessageResponse message)
        {
            if (message.teamInfo == null)
            {
                message.teamInfo = new TeamInfoResponse();
                message.teamInfo.Result = Result.Success;
                message.teamInfo.Team = new NTeamInfo();
                message.teamInfo.Team.Id = this.Id;
                message.teamInfo.Team.Leader = this.Leader.Id;
                foreach (var member in this.Members)
                {
                    message.teamInfo.Team.Members.Add(member.GetBasicInfo());
                }
            }
        }
    }
}

Character更改

public Team Team;
public int TeamUpdateTS;
public void PostProcess(NetMessageResponse message)
{
    Log.InfoFormat("PostProcess > CharacterID:{0}:{1}", this.Id, this.Info.Name);
    this.FriendManager.PostProcess(message);
    if (this.Team != null)
    {
        Log.InfoFormat("PostProcess > Team: characterID:{0}:{1}", this.Id, this.Info.Name, TeamUpdateTS, this.Team.timestamp);
        if (TeamUpdateTS < this.Team.timestamp)
        {
            TeamUpdateTS = Team.timestamp;
            this.Team.PostProcess(message);
        }
    }
    if (this.StatusManager.HasStatus)
    {
        this.StatusManager.PostProcess(message);
    }
}
public NCharacterInfo GetBasicInfo()
{
    return new NCharacterInfo()
    {
        Id = this.Id,
        Name = this.Info.Name,
        Class = this.Info.Class,
        Level = this.Info.Level
    };
}

客户端

TeamService:

using System;
using UnityEngine;
using Managers;
using Models;
using Network;
using SkillBridge.Message;
using UnityEngine.Events;

namespace Services
{
    class TeamService : Singleton<TeamService>,IDisposable
    {
        public UnityAction OnFriendUpdate;
        public void Init()
        {

        }
        public TeamService()
        {
            MessageDistributer.Instance.Subscribe<TeamInviteRequest>(this.OnTeamInviteRequest);
            MessageDistributer.Instance.Subscribe<TeamInviteResponse>(this.OnTeamInviteResponse);
            MessageDistributer.Instance.Subscribe<TeamInfoResponse>(this.OnTeamInfo);
            MessageDistributer.Instance.Subscribe<TeamLeaveResponse>(this.OnTeamLeave);

        }
        public void Dispose()
        {
            MessageDistributer.Instance.Unsubscribe<TeamInviteRequest>(this.OnTeamInviteRequest);
            MessageDistributer.Instance.Unsubscribe<TeamInviteResponse>(this.OnTeamInviteResponse);
            MessageDistributer.Instance.Unsubscribe<TeamInfoResponse>(this.OnTeamInfo);
            MessageDistributer.Instance.Unsubscribe<TeamLeaveResponse>(this.OnTeamLeave);
        }
        public void SendTeamInviteRequest(int friendId,string friendName)
        {
            Debug.Log("SendTeamInviteRequest");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.teamInviteReq = new TeamInviteRequest();
            message.Request.teamInviteReq.FromId = User.Instance.CurrentCharacter.Id;
            message.Request.teamInviteReq.FromName = User.Instance.CurrentCharacter.Name;
            message.Request.teamInviteReq.ToId = friendId;
            message.Request.teamInviteReq.ToName = friendName;
            NetClient.Instance.SendMessage(message);
        }
        public void SendTeamInviteResponse(bool accept, TeamInviteRequest request)
        {
            Debug.Log("SendFriendAddResponse");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.teamInviteRes = new TeamInviteResponse();
            message.Request.teamInviteRes.Result = accept ? Result.Success : Result.Failed;
            message.Request.teamInviteRes.Errormsg = accept ? "对方同意了您的组队申请" : "对方拒绝了你的组队申请";
            message.Request.teamInviteRes.Request = request;
            NetClient.Instance.SendMessage(message);
        }

        private void OnTeamInviteRequest(object sender, TeamInviteRequest request)
        {
            var confirm = MessageBox.Show(string.Format("[{0}]邀请你加入队伍", request.FromName), "组队请求", MessageBoxType.Confirm, "接受", "拒绝");
            confirm.OnYes = () => { this.SendTeamInviteResponse(true, request); };
            confirm.OnNo = () => { this.SendTeamInviteResponse(false, request); };
        }

        private void OnTeamInviteResponse(object sender, TeamInviteResponse message)
        {
            if (message.Result == Result.Success)
            {
                MessageBox.Show(message.Errormsg, "邀请组队成功");
            }
            else
            {
                MessageBox.Show(message.Errormsg,"邀请组队失败");
            }
        }

        private void OnTeamInfo(object sender, TeamInfoResponse message)
        {
            Debug.Log("OnTeamInfo");
            TeamManager.Instance.UpdateTeamInfo(message.Team);
        }

        public void SendTeamLeaveRequest(int id)
        {
            Debug.Log("SendTeamLeaveRequest");
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.teamLeave = new TeamLeaveRequest();
            message.Request.teamLeave.TeamId = User.Instance.TeamInfo.Id;
            message.Request.teamLeave.characterId = User.Instance.CurrentCharacter.Id;
            NetClient.Instance.SendMessage(message);
        }

        private void OnTeamLeave(object sender, TeamLeaveResponse message)
        {
            if (message.Result == Result.Success)
            {
                MessageBox.Show(message.Errormsg, "退出队伍");
                TeamManager.Instance.UpdateTeamInfo(null);
            }
            else
                MessageBox.Show(message.Errormsg, "退出队伍", MessageBoxType.Error);
        }

    }
}

TeamManager:

using System;
using System.Collections.Generic;
using SkillBridge.Message;
using Models;

namespace Managers
{
    class TeamManager : Singleton<TeamManager>
    {
        public void Init()
        {

        }
       
        public void UpdateTeamInfo(NTeamInfo team)
        {
            User.Instance.TeamInfo = team;
            ShowTeamUI(team != null);
            
        }
        public void ShowTeamUI(bool show)
        {
            if (UIMain.Instance != null)
            {
                UIMain.Instance.ShowTeamUI(show);
            }
        }
    }
}

UI层:UITeam

using Models;
using Services;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UITeam : MonoBehaviour
{
    public Text teamTitle;
    public UITeamItem[] Members;
    public ListView list;
    void Start()
    {
        foreach (var item in Members)
        {
            this.list.AddItem(item);
        }
        if (User.Instance.TeamInfo == null)
        {
            this.gameObject.SetActive(false);
        }      
    }
    private void OnEnable()
    {
        UpdateTeamUI();
    }

    public void ShowTeam(bool show)   // 这个函数感觉可以不用
    {
        this.gameObject.SetActive(show);
        if (show)
        {
            UpdateTeamUI();
        }
    }

    public void UpdateTeamUI()
    {
        if (User.Instance.TeamInfo == null) return;
        this.teamTitle.text = string.Format("我的队伍({0}/5)", User.Instance.TeamInfo.Members.Count);
        for (int i = 0; i < 5; i++)
        {
            if (i < User.Instance.TeamInfo.Members.Count)
            {
                this.Members[i].SetMemberInfo(i, User.Instance.TeamInfo.Members[i], User.Instance.TeamInfo.Members[i].Id == User.Instance.TeamInfo.Leader);
                this.Members[i].gameObject.SetActive(true);
            }
            else
                this.Members[i].gameObject.SetActive(false);
        }
    }

    public void OnClickLeave()
    {
        UIMessageBox uIMessageBox =  MessageBox.Show("确定要离开队伍吗?", "退出队伍", MessageBoxType.Confirm, "确定离开", "取消");
        uIMessageBox.OnYes = () => { TeamService.Instance.SendTeamLeaveRequest(User.Instance.TeamInfo.Id); };
    }
}

UI层:UITeamItem

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using SkillBridge.Message;

public class UITeamItem : ListView.ListViewItem
{
    public Text nickname;

    public Image classIcon;
    public Image leaderIcon;
    public Image background;

    public override void onSelected(bool selected)
    {
        this.background.enabled = selected ? true : false;
    }
    public int idx;
    public NCharacterInfo Info;

    private void Start()
    {
        this.background.enabled = false;
    }
    public void SetMemberInfo(int idx,NCharacterInfo item,bool isLeader)
    {
        this.idx = idx;
        this.Info = item;
        if (this.nickname != null) this.nickname.text = this.Info.Level.ToString().PadRight(4) + this.Info.Name;
        if (this.classIcon != null) this.classIcon.overrideSprite = SpriteManager.Instance.ClassIcons[(int)this.Info.Class - 1];
        if (this.leaderIcon != null) this.leaderIcon.gameObject.SetActive(isLeader);
    }
}

公会系统

需求分析

数据结构

数据库:

协议:

message NetMessageRequest{
	GuildCreateRequest guildCreate = 23;
	GuildJoinRequest guildJoinReq = 24;
	GuildJoinResponse guildJoinRes = 25;
	GuildRequest guild = 26;
	GuildLeaveRequest guildLeave = 27;
	GuildListRequest guildList = 28;
	GuildAdminRequest guildAdmin = 29;
	GuildNoticeRequest guildNotice = 30;
}

message NetMessageResponse{
	GuildCreateResponse guildCreate = 23;
	GuildJoinRequest guildJoinReq = 24;
	GuildJoinResponse guildJoinRes = 25;
	GuildResponse guild = 26;
	GuildLeaveResponse guildLeave = 27;
	GuildListResponse guildList = 28;
	GuildAdminResponse guildAdmin = 29;
	GuildNoticeResponse guildNotice = 30;
}
// Guild System
enum GUILD_TITLE {
	NONE = 0;
	PRESIDENT = 1;
	VICE_PRESIDENT = 2;
}
enum APPLY_RESULT{
	NONE = 0;
	ACCEPT = 1;
	REJECT = 2;
}
// 公会信息
message NGuildInfo{
	int32 id = 1;//公会ID
	string guild_name = 2;//公会名称
	int32 leaderId = 3;//会长ID
	string leaderName = 4;//会长名称
	string notice = 5;//公会宗旨
	int32 memberCount = 6;//成员人数
	repeated NGuildMemberInfo members = 7;//成员列表
	repeated NGuildApplyInfo applies = 8;//申请信息
	int64 createTime = 9;//创建事件
}
//成员信息
message NGuildMemberInfo{
	int32 id = 1;
	int32 characterId = 2;
	GUILD_TITLE title = 3;//职位
	NCharacterInfo info = 4;//角色信息
	int64 joinTime = 5;//加入时间
	int64 lastTime = 6;//上次上线时间
	int32 status = 7;//在线状态
}
//申请信息
message NGuildApplyInfo{
	int32 guild_id = 1;
	int32 characterId = 2;
	string name = 3;
	int32 class = 4;
	int32 level = 5;
	APPLY_RESULT result = 6;
}
message GuildCreateRequest{
	string guild_name = 1;
	string guild_notice = 2;
}
message GuildCreateResponse{
	RESULT result = 1;
	string errormsg = 2;
	NGuildInfo guildInfo = 3;
}
message GuildNoticeRequest{
	string guild_notice = 2;
}
message GuildNoticeResponse{
	RESULT result = 1;
	string errormsg = 2;
}
message GuildJoinRequest{
	NGuildApplyInfo apply = 1;
}
message GuildJoinResponse{
	RESULT result = 1;
	string errormsg = 2;
	NGuildApplyInfo apply = 3;
}
// 公会列表
message GuildListRequest{

}
message GuildListResponse{
	RESULT result = 1;
	string errormsg = 2;
	repeated NGuildInfo guilds = 3;
}
// 公会信息
message GuildRequest{

}
message GuildResponse{
	RESULT result = 1;
	string errormsg = 2;
	NGuildInfo guildInfo = 3;
}
message GuildLeaveRequest{

}
message GuildLeaveResponse{
	RESULT result = 1;
	string errormsg = 2;
}

enum GUILD_ADMIN_COMMAND{
	KICKOUT = 1;
	PROMOTE = 2;
	DEPOST = 3;
	TRANSFER = 4;
}
message GuildAdminRequest{
	GUILD_ADMIN_COMMAND command = 1;
	int32 target = 2;
}
message GuildAdminResponse{
	RESULT result = 1;
	string errormsg = 2;
	GuildAdminRequest command = 3;
}

代码实现

这里代码太多(特别是UI层)详情看具体文件。
客户端除了UI层只有GuildManager和GuildService.
客户端除了GuildManager和GuildService还有一个Guild,信息通知同样使用了后处理框架。

聊天系统

需求分析

数据添加

协议:

message NetMessageRequest{
	ChatRequest chat = 31;
}

message NetMessageResponse{
	ChatResponse chat = 31;
}

// chat
// 这里设置成2的n次方,可以任意组合频道,比如3就是LOCAL+WORLD
enum CHAT_CHANNEL{
	All = -1;
	LOCAL = 1;
	WORLD = 2;
	SYSTEM = 4;
	PRIVATE = 8;
	TEAM = 16;
	GUILD = 32;
}

message ChatMessage{
	CHAT_CHANNEL channel = 1;
	int32 id = 2;
	int32 from_id = 3;
	string from_name = 4;
	int32 to_id = 5;
	string to_name = 6;
	string message = 7;
	double time = 8;
}
message ChatRequest{
	ChatMessage message = 1;
}
message ChatResponse{
	RESULT result = 1;
	string errormsg = 2;
	repeated ChatMessage localMessages = 3;
	repeated ChatMessage worldMessages = 4;
	repeated ChatMessage systemMessages = 5;
	repeated ChatMessage privateMessages = 6;
	repeated ChatMessage teamMessages = 7;
	repeated ChatMessage guildMessages = 8;
}

聊天插件

名称:Candlelight,用于显示聊天文本,支持超链接
添加UI:UI->CandleLight->HyperText,主要使用这个UI组件
组件重要的属性:
Styles:定义了文本的样式,可以右键Create->CandleLight->HyperTextStyle创建样式,其中包含了超文本中的文字的类型及其颜色,上下标等。
OnClick:点击对应的超链接文本的时候会触发的事件。其他类似的有OnEnter,OnExit,OnPress,OnRelease。
Text:显示聊天文字的地方,支持超链接。
注意:这个UI组件往往和Content Size Fitter一起使用,便于灵活调节大小。

代码实现

客户端

UI层:UIPopCharMenu

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Managers;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Services;

public class UIPopCharMenu : UIWindow,IDeselectHandler  // 与SelectHandler相反,这是取消选中后的处理
{
    public int targetId;
    public string targetName;

    public void OnDeselect(BaseEventData eventData) // 点击任何其他地方都会取消选中,导致这个方法执行
    {
        var ed = eventData as PointerEventData;
        if (ed.hovered.Contains(this.gameObject))  // 判断点击事件所包含的所有节点是否包含我们当前的物体(其子物体也算当前物体)
            return;
        this.Close(WindowResult.None);             // 如果点在框外面就关闭窗口
    }
    // 一开始就把窗口设置为选中状态
    public void OnEnable()
    {
        this.GetComponent<Selectable>().Select();
        this.Root.transform.position = Input.mousePosition + new Vector3(80, 0, 0);
    }

    public void OnChat()
    {
        ChatManager.Instance.StartPrivateChat(targetId, targetName);
        this.Close(WindowResult.No);
    }
    public void OnAddFriend()
    {
        FriendService.Instance.SendFriendAddRequest(targetId, targetName);
        this.Close(WindowResult.No);
    }
    public void OnInviteTeam()
    {
        TeamService.Instance.SendTeamInviteRequest(targetId, targetName);
        this.Close(WindowResult.No);
    }
}

UI层:UIChat

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Candlelight.UI;
using Managers;
using UnityEngine.UI;
using System;
using SkillBridge.Message;

public class UIChat : MonoBehaviour
{
    public HyperText textArea;
    public TabView channelTab;
    public InputField chatText;
    public Text chatTarget;
    public Dropdown channelSelect;

    void Start()
    {
        this.channelTab.OnTabSelect += OnDisplayChannelSelected;
        ChatManager.Instance.OnChat += RefreshUI;
    }
    private void OnDestroy()
    {
        ChatManager.Instance.OnChat -= RefreshUI;
    }
    // 设置对应的聊天频道,并刷新UI
    private void OnDisplayChannelSelected(int idx)
    {
        ChatManager.Instance.displayChannel = (ChatManager.LocalChannel)idx;
        RefreshUI();
    }

    void Update()
    {
        InputManager.Instance.IsInputMode = chatText.isFocused;  // 检测输入框是否拥有焦点
    }

    public void RefreshUI()
    {
        this.textArea.text = ChatManager.Instance.GetCurrentMessages();
        this.channelSelect.value = (int)ChatManager.Instance.sendChannel - 1;
        if (ChatManager.Instance.SendChannel == ChatChannel.Private)
        {
            this.chatTarget.gameObject.SetActive(true);
            if (ChatManager.Instance.PrivateID != 0)
                this.chatTarget.text = ChatManager.Instance.PrivateName + ":";
            else
                this.chatTarget.text = "<无>";
        }
        else
            this.chatTarget.gameObject.SetActive(false);
    }
    // 点击超链接,弹出小窗口,附上对应值
    public void OnClickChatLink(HyperText text,HyperText.LinkInfo link)
    {
        if (string.IsNullOrEmpty(link.Name))
            return;
        // 这里name作了约定,c:开头表示任务,i:开头表示道具,例如c:1001:Name,i:1001:Name
        // <a name="c:1001:Name" class="player">Name</a>这里点击了玩家的名字
        if (link.Name.StartsWith("c:"))
        {
            string[] strs = link.Name.Split(":".ToCharArray()); // 拆分字符串
            UIPopCharMenu menu = UIManager.Instance.Show<UIPopCharMenu>();
            menu.targetId = int.Parse(strs[1]);
            menu.targetName = strs[2];
        }
    }
    // 输入框结束输入(按回车键)的事件可以绑定这个方法
    public void OnClickSend()
    {
        OnEndInput(this.chatText.text);
    }
    public void OnEndInput(string text)
    {
        if (!string.IsNullOrEmpty(text.Trim()))
            this.SendChat(text);
        this.chatText.text = "";
    }
    void SendChat(string content)
    {
        ChatManager.Instance.SendChat(content, ChatManager.Instance.PrivateID, ChatManager.Instance.PrivateName);
    }

    // 下拉框绑定事件
    public void OnSendChannelChanged()
    {
        int idx = channelSelect.value;
        if (ChatManager.Instance.sendChannel == (ChatManager.LocalChannel)(idx + 1))
            return;
        // 设置成功了刷新界面,没成功要恢复值
        if (!ChatManager.Instance.SetSendChannel((ChatManager.LocalChannel)idx + 1))
            this.channelSelect.value = (int)ChatManager.Instance.sendChannel - 1;
        else
            this.RefreshUI();
        
    }
}

Manager层:CharManager

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Models;
using SkillBridge.Message;
using UnityEngine;
using Services;
using Common.Data;

namespace Managers
{
    class ChatManager : Singleton<ChatManager>
    {
        public enum LocalChannel
        {
            All = 0,
            Local = 1,
            World = 2,
            Team = 3,
            Guild = 4,
            Private = 5,
        }

        private ChatChannel[] ChannelFilter = new ChatChannel[6]
        {
            ChatChannel.Local | ChatChannel.World | ChatChannel.Team | ChatChannel.Guild | ChatChannel.Private | ChatChannel.System,
            ChatChannel.Local,
            ChatChannel.World,
            ChatChannel.Team,
            ChatChannel.Guild,
            ChatChannel.Private
        };
        // 一个消息列表,一个展示频道,一个输出频道
        public List<ChatMessage>[] Messages = new List<ChatMessage>[6]{
            new List<ChatMessage>(),
            new List<ChatMessage>(),
            new List<ChatMessage>(),
            new List<ChatMessage>(),
            new List<ChatMessage>(),
            new List<ChatMessage>()
        };
        public LocalChannel displayChannel;
        public LocalChannel sendChannel;

        public int PrivateID = 0;
        public string PrivateName = "";

        internal void StartPrivateChat(int targetId,string targetName)
        {
            this.PrivateID = targetId;
            this.PrivateName = targetName;
            this.sendChannel = LocalChannel.Private;
            if (this.OnChat != null)
                this.OnChat();
        }

        public ChatChannel SendChannel
        {
            get
            {
                switch (sendChannel)
                {
                    case LocalChannel.Local:
                        return ChatChannel.Local;
                    case LocalChannel.World:
                        return ChatChannel.World;
                    case LocalChannel.Team:
                        return ChatChannel.Team;
                    case LocalChannel.Guild:
                        return ChatChannel.Guild;
                    case LocalChannel.Private:
                        return ChatChannel.Private;
                }
                return ChatChannel.Local;
            }
        }

        public Action OnChat { get; internal set; }

        public void Init()
        {
            foreach (var messages in this.Messages)
            {
                messages.Clear();
            }
        }
        public void SendChat(string content,int toId = 0,string toName = "")
        {
            if(sendChannel == LocalChannel.Team)
            {
                if (User.Instance.TeamInfo == null)
                {
                    this.AddSystemMessage("你没有加入任何队伍");
                    return;
                }
            }
            else if (sendChannel == LocalChannel.Guild)
            {
                if (GuildManager.Instance.guildInfo == null)
                {
                    this.AddSystemMessage("你没有加入任何公会");
                    return;
                }
            }
            ChatService.Instance.SendChat(this.SendChannel, content, toId, toName);
        }

        // 设置发送频道,并判断能不能设置成功
        public bool SetSendChannel(LocalChannel channel)
        {
            if (channel == LocalChannel.Team)
            {
                if (User.Instance.TeamInfo == null)
                {
                    this.AddSystemMessage("你没有加入任何队伍");
                    return false;
                }
            }
            if (channel == LocalChannel.Guild)
            {
                if (GuildManager.Instance.guildInfo == null)
                {
                    this.AddSystemMessage("你没有加入任何公会");
                    return false;
                }
            }
            this.sendChannel = channel;
            Debug.LogFormat("Set Channel:{0}", this.sendChannel);
            return true;
        }
        // 接受服务端的消息,过滤处理,添加系统以外的信息(注意这种过滤的用法)
        internal void AddMessages(ChatChannel channel,List<ChatMessage> messages)
        {
            for (int ch = 0; ch < 6; ch++)
            {
                if ((this.ChannelFilter[ch] & channel) == channel)
                {
                    this.Messages[ch].AddRange(messages);
                    // 消息过多则清除
                    if (Messages[ch].Count > GameDefine.MaxChatSaveNums_Client)
                        Messages[ch].RemoveRange(0, Messages[ch].Count - GameDefine.MaxChatSaveNums_Client);
                }
                if (this.OnChat != null)
                    this.OnChat();
            }
        }
        // 用于添加服务端的报错信息
        public void AddSystemMessage(string message,string from = "")
        {
            this.Messages[(int)LocalChannel.All].Add(new ChatMessage()  // 客户端没有系统频道,所以添加到All中
            {
                Channel = ChatChannel.System,
                Message = message,
                FromName = from
            });
            if (this.OnChat != null)
                this.OnChat();
        }
        // 获取当前频道的信息,用于给聊天框赋值(入口)
        public string GetCurrentMessages()
        {
            StringBuilder sb = new StringBuilder();
            foreach (var message in this.Messages[(int)displayChannel])
            {
                sb.AppendLine(FormatMessage(message));
            }
            return sb.ToString();
        }
        // 把所有的信息转换成插件的格式
        private string FormatMessage(ChatMessage message)
        {
            switch (message.Channel)
            {
                case ChatChannel.All:
                    break;
                case ChatChannel.Local:
                    return string.Format("[本地]{0}{1}", FormatFromPlayer(message), message.Message);
                case ChatChannel.World:
                    return string.Format("<color=cyan>[世界]{0}{1}</color>", FormatFromPlayer(message), message.Message);
                case ChatChannel.System:
                    return string.Format("<color=yellow>[系统]{0}</color>", message.Message);
                case ChatChannel.Private:
                    return string.Format("<color=magenta>[私聊]{0}{1}</color>", FormatFromPlayer(message), message.Message);
                case ChatChannel.Team:
                    return string.Format("<color=green>[队伍]{0}{1}</color>", FormatFromPlayer(message), message.Message);
                case ChatChannel.Guild:
                    return string.Format("<color=blue>[公会]{0}{1}</color>", FormatFromPlayer(message), message.Message);
            }
            return "";

        }
        // 生成角色的超链接
        private string FormatFromPlayer(ChatMessage message)
        {
            if (message.FromId == User.Instance.CurrentCharacter.Id)
                return "<a name=\"\" class=\"player\">[我]</a>";
            else
                return string.Format("<a name=\"c:{0}:{1}\" class=\"player\">[{1}]</a>", message.FromId, message.FromName);
        }
    }
}

Service层:CharService

using Managers;
using Network;
using SkillBridge.Message;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Services
{
    class ChatService : Singleton<ChatService>,IDisposable
    {
        public void Init()
        {

        }
        public ChatService()
        {
            MessageDistributer.Instance.Subscribe<ChatResponse>(this.OnChat);   
        }
        public void Dispose()
        {
            MessageDistributer.Instance.Unsubscribe<ChatResponse>(this.OnChat);
        }

        public void SendChat(ChatChannel channel, string content, int toId, string toName)
        {
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.Chat = new ChatRequest();
            message.Request.Chat.Message = new ChatMessage();
            message.Request.Chat.Message.Channel = channel;
            message.Request.Chat.Message.ToId = toId;
            message.Request.Chat.Message.ToName = toName;
            message.Request.Chat.Message.Message = content;
            NetClient.Instance.SendMessage(message);
        }

        private void OnChat(object sender,ChatResponse message)
        {
            if (message.Result == Result.Success)
            {
                ChatManager.Instance.AddMessages(ChatChannel.Local, message.localMessages);
                ChatManager.Instance.AddMessages(ChatChannel.World, message.worldMessages);
                ChatManager.Instance.AddMessages(ChatChannel.System, message.systemMessages);
                ChatManager.Instance.AddMessages(ChatChannel.Private, message.privateMessages);
                ChatManager.Instance.AddMessages(ChatChannel.Team, message.teamMessages);
                ChatManager.Instance.AddMessages(ChatChannel.Guild, message.guildMessages);
            }
            else
                ChatManager.Instance.AddSystemMessage(message.Errormsg);
        }
    }
}

服务器

using SkillBridge.Message;
using Common;
using Common.Data;
using Network;
using GameServer.Entities;
using GameServer.Managers;

namespace GameServer.Services
{
    class ChatService:Singleton<ChatService>
    {
        public ChatService()
        {
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<ChatRequest>(this.OnChat);
        }

        public void Init()
        {
            ChatManager.Instance.Init();
        }
        private void OnChat(NetConnection<NetSession> sender, ChatRequest request)
        {
            Character character = sender.Session.Character;
            Log.InfoFormat("OnChat: character:{0}:Channel {1} Message:{2}", character.Id, request.Message.Channel,request.Message.Message);
            // 如果是私聊,直接发给对面,并发回自己一份
            if (request.Message.Channel == ChatChannel.Private)
            {
                var chatTo = SessionManager.Instance.GetSession(request.Message.ToId);
                if (chatTo == null)
                {
                    sender.Session.Response.Chat = new ChatResponse();
                    sender.Session.Response.Chat.Result = Result.Failed;
                    sender.Session.Response.Chat.Errormsg = "对方不在线";
                    sender.Session.Response.Chat.privateMessages.Add(request.Message);
                }
                else
                {
                    if (chatTo.Session.Response.Chat == null)
                        chatTo.Session.Response.Chat = new ChatResponse();
                    request.Message.FromId = character.Id;
                    request.Message.FromName = character.Name;
                    chatTo.Session.Response.Chat.Result = Result.Success;
                    chatTo.Session.Response.Chat.privateMessages.Add(request.Message);
                    chatTo.SendResponse();
                    if (sender.Session.Response.Chat == null)
                        sender.Session.Response.Chat = new ChatResponse();
                    sender.Session.Response.Chat.Result = Result.Success;
                    sender.Session.Response.Chat.privateMessages.Add(request.Message);
                    sender.SendResponse();
                }
            }
            // 如果不是私聊,添加到服务器内存中
            else
            {
                sender.Session.Response.Chat = new ChatResponse();
                sender.Session.Response.Chat.Result = Result.Success;
                ChatManager.Instance.AddMessage(character, request.Message);
                sender.SendResponse();
            }
        }
    }
}
using Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SkillBridge.Message;
using GameServer.Entities;
using GameServer.Managers;
using Common.Utils;
using Common.Data;

namespace GameServer.Managers
{
    class ChatManager : Singleton<ChatManager>
    {
        public List<ChatMessage> System = new List<ChatMessage>();
        public List<ChatMessage> World = new List<ChatMessage>();
        public Dictionary<int, List<ChatMessage>> Local = new Dictionary<int, List<ChatMessage>>();
        public Dictionary<int, List<ChatMessage>> Team = new Dictionary<int, List<ChatMessage>>();
        public Dictionary<int, List<ChatMessage>> Guild = new Dictionary<int, List<ChatMessage>>();

        public void Init()
        {

        }

        public void AddMessage(Character from,ChatMessage message)
        {
            message.FromId = from.Id;
            message.FromName = from.Name;
            message.Time = TimeUtil.timestamp;

            switch (message.Channel)
            {
                case ChatChannel.Local:
                    this.AddLocalMessage(from.Info.mapId, message);
                    break;
                case ChatChannel.World:
                    this.AddWorldMessage(message);
                    break;
                case ChatChannel.System:
                    this.AddSystemMessage(message);
                    break;
                case ChatChannel.Team:
                    if (from.Team != null)
                        this.AddTeamMessage(from.Team.Id, message);
                    break;
                case ChatChannel.Guild:
                    if(from.Guild != null)
                        this.AddGuildMessage(from.Guild.Id, message);
                    break;

            }
        }

        public void AddLocalMessage(int mapId,ChatMessage message)
        {
            if (!this.Local.TryGetValue(mapId,out List<ChatMessage> messages))
            {
                messages = new List<ChatMessage>();
                this.Local[mapId] = messages;
            }
            messages.Add(message);
        }
        public void AddSystemMessage(ChatMessage message)
        {
            this.System.Add(message);
        }
        public void AddWorldMessage(ChatMessage message)
        {
            this.World.Add(message);
        }

        public void AddGuildMessage(int guildId, ChatMessage message)
        {
            if (!this.Guild.TryGetValue(guildId, out List<ChatMessage> messages))
            {
                messages = new List<ChatMessage>();
                this.Guild[guildId] = messages;
            }
            messages.Add(message);
        }

        public void AddTeamMessage(int teamId, ChatMessage message)
        {
            if (!this.Team.TryGetValue(teamId, out List<ChatMessage> messages))
            {
                messages = new List<ChatMessage>();
                this.Team[teamId] = messages;
            }
            messages.Add(message);
        }

        public int GetLocalMessages(int mapId,int idx,List<ChatMessage> result)
        {
            if (!this.Local.TryGetValue(mapId,out List<ChatMessage> messages))
            {
                return 0;
            }
            return GetNewMessages(idx, result, messages);
        }

        public int GetWorldMessages(int idx,List<ChatMessage> result)
        {
            return GetNewMessages(idx, result, this.World);
        }
        public int GetSystemMessages(int idx, List<ChatMessage> result)
        {
            return GetNewMessages(idx, result, this.System);
        }
        public int GetTeamMessages(int teamId, int idx, List<ChatMessage> result)
        {
            if (!this.Team.TryGetValue(teamId, out List<ChatMessage> messages))
            {
                return 0;
            }
            return GetNewMessages(idx, result, messages);
        }
        public int GetGuildMessages(int guildId, int idx, List<ChatMessage> result)
        {
            if (!this.Guild.TryGetValue(guildId, out List<ChatMessage> messages))
            {
                return 0;
            }
            return GetNewMessages(idx, result, messages);
        }
        // 如果消息大于容纳量,就只放最大的容量
        private int GetNewMessages(int idx,List<ChatMessage> result,List<ChatMessage> messages)
        {
            if (idx == 0)  // 拉取最大数量
            {
                if (messages.Count > GameDefine.MaxChatRecordNums)
                    idx = messages.Count - GameDefine.MaxChatRecordNums;
            }
            for (; idx < messages.Count; idx++) // 如果idx不等于0,我们可以指定拉很少的条数
            {
                result.Add(messages[idx]);
            }
            // 元素过多则移除
            if(messages.Count > GameDefine.MaxChatSaveNums)
            {
                messages.RemoveRange(0, 50);
                idx -= 50;
            }
            return idx;
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GameServer.Entities;
using SkillBridge.Message;
using GameServer.Managers;

namespace GameServer.Models
{
    class Chat
    {
        Character Owner;
        // 拉到第几条信息
        public int localIdx;
        public int worldIdx;
        public int systemIdx;
        public int teamIdx;
        public int guildIdx;

        public Chat(Character owner)
        {
            this.Owner = owner;
        }
        // 这个后处理一直会执行,不设条件,每次拉的都是最新的记录,已经拉的不会再拉
        public void PostProcess(NetMessageResponse message)
        {
            if (message.Chat == null)
            {
                message.Chat = new ChatResponse();
                message.Chat.Result = Result.Success;
            }
            this.localIdx = ChatManager.Instance.GetLocalMessages(this.Owner.Info.mapId, this.localIdx, message.Chat.localMessages);
            this.worldIdx = ChatManager.Instance.GetWorldMessages(this.worldIdx, message.Chat.worldMessages);
            this.systemIdx = ChatManager.Instance.GetSystemMessages(this.systemIdx, message.Chat.systemMessages);
            if(this.Owner.Team != null)
                this.teamIdx = ChatManager.Instance.GetTeamMessages(this.Owner.Team.Id, this.teamIdx, message.Chat.teamMessages);
            if (this.Owner.Guild != null)
                this.guildIdx = ChatManager.Instance.GetGuildMessages(this.Owner.Guild.Id, this.guildIdx, message.Chat.guildMessages);
        }
    }
}

其他系统

坐骑系统

需求分析与设计

坐骑没有做Service和Manager,因为太轻量了可以把代码放到一起,坐骑可以看作是物品的一种,只是可以从坐骑面板中看到而已。因此我们无需添加额外的协议和数据库,直接利用道具系统、商店系统以及移动同步就能把坐骑系统完成

坐骑与人物动画

这里有一个关于状态机的新知识点,那就是动画的层。
我们可以无需给骑坐骑时人物的动作创建新的动画节点,我们只需要创建一个新的层,就能在原有的动画节点上添加新的动画,上骑下骑时切换动画的层即可,Layer中设置的Sync勾选上,就能混合两个层,靠权重调节播放哪一层的动画,或是混合起来的动画都可以。

代码

服务器:
Map:

internal void UpdateEntity(NEntitySync entity)
{
    // 把自己的位置更新到服务器,并发送信息给别人
    foreach (var kv in this.MapCharacters)
    {
        if(kv.Value.character.entityId == entity.Id)
        {
            kv.Value.character.Position = entity.Entity.Position;
            kv.Value.character.Direction = entity.Entity.Direction;
            kv.Value.character.Speed = entity.Entity.Speed;
            // 上马状态就把马的Id传过来给角色
            if (entity.Event == EntityEvent.Ride)
                kv.Value.character.Ride = entity.Param;
        }
        else
        {
            MapService.Instance.SendEntityUpdate(kv.Value.connection, entity);
        }
    }
}

客户端:
UI层:UIRide

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Managers;
using SkillBridge.Message;
using Models;

public class UIRide : UIWindow {

    public Text descript;
    public GameObject itemPrefab;
    public ListView listMain;
    private UIRideItem selectedItem;
    public Text rideButtonText;

    void Start () {
        RefreshUI();
        this.listMain.onItemSelected += this.ItemSelected;
	}

    private void RefreshUI()
    {
        ClearItems();
        InitItems();
    }

    private void InitItems()
    {
        // 坐骑这里视为道具的一种,一般需要分离出来,这样需要另外建立一个坐骑的数据库和坐骑的类保存,视为道具代码量会减少非常多
        foreach (var kv in ItemManager.Instance.Items)
        {
            if(kv.Value.Define.Type == ItemType.Ride && (kv.Value.Define.LimitClass == CharacterClass.None || kv.Value.Define.LimitClass == User.Instance.CurrentCharacter.Class))
            {
                GameObject go = Instantiate(itemPrefab, this.listMain.transform);
                UIRideItem ui = go.GetComponent<UIRideItem>();
                ui.SetRideItem(kv.Value, this, false);
                this.listMain.AddItem(ui);
            }
        }
    }

    private void ClearItems()
    {
        listMain.RemoveAll();
    }

    private void ItemSelected(ListView.ListViewItem item)
    {
        this.selectedItem = item as UIRideItem;
        this.descript.text = this.selectedItem.item.Define.Description;
        if (this.selectedItem.item.Id == User.Instance.CurrentRide)
            rideButtonText.text = "召回坐骑";
        else
            rideButtonText.text = "召唤坐骑";
    }
    public void DoRide()
    {
        if (this.selectedItem == null)
        {
            MessageBox.Show("请选择要召唤的坐骑", "提示");
            return;
        }
        User.Instance.Ride(this.selectedItem.item.Id);
        if (User.Instance.CurrentRide != 0)
            rideButtonText.text = "召回坐骑";
        else
            rideButtonText.text = "召唤坐骑";
    }
}

UI层:UIRideItem

using Common.Data;
using Models;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class UIRideItem : ListView.ListViewItem
{
    public Image icon;
    public Text title;
    public Text level;

    public Image backgound;
    public Sprite normalBg;
    public Sprite selectedBg;
    //UIRide owner;

    public Item item;
    public override void onSelected(bool selected)     // 接口特有方法,选中的时候执行,也可以用UI来执行这个操作
    {
        this.backgound.overrideSprite = selected ? selectedBg : normalBg;
    }

    public void SetRideItem(Item item,UIRide owner,bool equiped)
    {
        this.item = item;
        if(title != null) this.title.text = this.item.Define.Name;
        if (level != null) this.level.text = "Lv " + this.item.Define.Level.ToString();
        if (icon != null) this.icon.overrideSprite = Resloader.Load<Sprite>(this.item.Define.Icon);
    }
}

User新增


public int CurrentRide = 0;
internal void Ride(int id)
{
    if (CurrentRide != id)
    {              
        CurrentCharacterObject.SendEntityEvent(EntityEvent.Ride, id);
        CurrentRide = id;
    }
    else  // 下马
    {
        CurrentCharacterObject.SendEntityEvent(EntityEvent.Ride, 0);
        CurrentRide = 0;
    }
}

PlayInputController

public void SendEntityEvent(EntityEvent entityEvent,int param = 0)
{
    // 用来改变动画状态
    if (entityController != null)
        entityController.OnEntityEvent(entityEvent,param);
    MapService.Instance.SendMapEntitySync(entityEvent, this.character.EntityData,param);
}

EntityController

public RideController rideController;
private int currentRide = 0;
public Transform rideBone;  // 拿屁股跟马对齐
public void OnEntityEvent(EntityEvent entityEvent, int param)
{
    switch(entityEvent)
    {
        case EntityEvent.Idle:
            anim.SetBool("Move", false);
            anim.SetTrigger("Idle");
            break;
        case EntityEvent.MoveFwd:
            anim.SetBool("Move", true);
            break;
        case EntityEvent.MoveBack:
            anim.SetBool("Move", true);
            break;
        case EntityEvent.Jump:
            anim.SetTrigger("Jump");
            break;
        case EntityEvent.Ride:
            this.Ride(param);
            break;
    }
    if (this.rideController != null) this.rideController.OnEntityEvent(entityEvent, param); // 把角色状态也转发一份给坐骑
}
public void Ride(int rideId)
{
    if (currentRide == rideId) return;        
    // 上马下马
    if (rideId > 0)
    {
        // 删除旧的,再添加新的
        if (currentRide != 0)
            Destroy(this.rideController.gameObject);
        this.rideController = GameObjectManager.Instance.LoadRide(rideId, this.transform);            
    }
    else
    {
        Destroy(this.rideController.gameObject);
        this.rideController = null;
    }
    if (this.rideController == null)
    {
        this.anim.transform.localPosition = Vector3.zero;
        // 设置层权重,对动画控制,设置第一层(骑乘层)权重为0(下马状态),第二层(上马状态)权重为1,在状态机中可以设置层
        this.anim.SetLayerWeight(1, 0); 
    }
    else
    {
        this.rideController.SetRider(this);
        this.anim.SetLayerWeight(1, 1);
    }
    currentRide = rideId;
}
// 保证角色跟着坐骑动,position指马上的乘骑位置
public void SetRidePosition(Vector3 position)
{
    this.anim.transform.position = position + (this.anim.transform.position - this.rideBone.position);
}

RideController

using SkillBridge.Message;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Entities;
using Managers;


public class RideController : MonoBehaviour
{

    public Animator anim;
    public EntityController rider;
    public Transform mountPoint;
    public Vector3 offset;


    // Use this for initialization
    void Start () {
        anim = this.GetComponent<Animator>();
    }

    void Update()
    {
        if (this.mountPoint == null || this.rider == null) return;
        this.rider.SetRidePosition(this.mountPoint.position + this.mountPoint.TransformDirection(this.offset)); // offset要根据坐骑位置mountPoint的方向做变换
    }
    public void SetRider(EntityController rider)
    {
        this.rider = rider;
    }
    public void OnEntityEvent(EntityEvent entityEvent, int param)
    {
        switch (entityEvent)
        {
            case EntityEvent.Idle:
                anim.SetBool("Move", false);
                anim.SetTrigger("Idle");
                break;
            case EntityEvent.MoveFwd:
                anim.SetBool("Move", true);
                break;
            case EntityEvent.MoveBack:
                anim.SetBool("Move", true);
                break;
            case EntityEvent.Jump:
                anim.SetTrigger("Jump");
                break;
        }
    }   
}

声音系统

知识点讲解

声音文件属性:
Load Type:加载类型,如果音乐文件比较大,建议使用Streaming不用一次性加载到内存中
CompressionFormat:压缩格式,PCM,ADPCM类似无损,不用解压直接播放,但是占用空间比较大,Vorbis是压缩格式,播放时解压,尺寸小,一般用Vorbis,可以调质量,质量越低,占用空间越小

混音器:Create->Audio Mixer创建,Window->Audio->Audio Mixer编辑
使用:

  1. 在Group中对着Master点右键就能创建通道,在Master的子节点下
  2. 每一个AudioSource除了AudioClip之外,就能指定一个Output,放置的就是Audio Mixer Group,通过操作Output我们可以决定在哪一个混音器通道做输出
  3. 在Inspector中右键混音器通道法的一个属性可以添加Exposed Parameters,在混音器界面的右上角可以查看,这样可以通过代码访问
  4. 混音器面板上,我们可以设置快照,例如我们每个场景中音乐通道和音效通道的音量大小不一样,我们可以设置快照进行切换,这种切换是平滑的

代码

using System;
class SoundDefine
{
    public const string Music_Login = "bgm-login";
    public const string Music_Select = "bgm-select";
    public const string SFX_Message_info = "ui/sfx_msg_info";
    public const string SFX_Message_Error = "ui/sfx_msg_error";
    public const string SFX_UI_Click = "ui/sfx_click1";
    public const string SFX_UI_Confirm = "ui/sfx_accept1";
    public const string SFX_UI_Win_Open = "ui/ui_win_show";
    public const string SFX_UI_WinClose = "ui/ui_win_close";
    public const string SFX_UI_Shop_Purchase = "ui/sfx_shop_purchase";
    public const string SFX_UI_Return = "ui/sfx_return1";

}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Audio;


class SoundManager : MonoSingleton<SoundManager>
{
    public AudioMixer audioMixer;
    public AudioSource musicAudioSource;
    public AudioSource soundAudioSource;

    const string MusicPath = "Music/";
    const string SoundPath = "Sound/";

    private bool musicOn;
    public bool MusicOn
    {
        get { return musicOn; }
        set
        {
            musicOn = value;
            this.MusicMute(!musicOn);
        }
    }
    private bool soundOn;
    public bool SoundOn
    {
        get { return soundOn; }
        set
        {
            soundOn = value;
            this.SoundMute(!soundOn);
        }
    }

    private int musicVolume;
    public int MusicVolume
    {
        get { return musicVolume; }
        set
        {
            if (musicVolume != value)
            {
                musicVolume = value;
                if (musicOn) this.SetVolume("MusicVolume", musicVolume);
            }
        }
    }
    private int soundVolume;
    public int SoundVolume
    {
        get { return soundVolume; }
        set
        {
            if (soundVolume != value)
            {
                soundVolume = value;
                if (SoundOn) this.SetVolume("SoundVolume", soundVolume);
            }
        }
    }

    private void Start()
    {
        this.MusicOn = Config.MusicOn;
        this.SoundOn = Config.SoundOn;
        this.MusicVolume = Config.MusicVolume;
        this.SoundVolume = Config.SoundVolume;
    }

    public void MusicMute(bool mute)
    {
        this.SetVolume("MusicVolume", mute ? 0 : musicVolume);
    }
    public void SoundMute(bool mute)
    {
        this.SetVolume("SoundVolume", mute ? 0 : soundVolume);
    }
    private void SetVolume(string name,int value)
    {
        float volume = value * 0.8f - 80f;
        this.audioMixer.SetFloat(name, volume);
    }
    public void PlayMusic(string name)
    {
        AudioClip clip = Resloader.Load<AudioClip>(MusicPath + name);
        if (clip == null)
        {
            Debug.LogWarningFormat("PlayMusic: {0} not existed", name);
            return;
        }
        if (musicAudioSource.isPlaying)
            musicAudioSource.Stop();
        musicAudioSource.clip = clip;
        musicAudioSource.Play();
    }
    public void PlaySound(string name)
    {
        AudioClip clip = Resloader.Load<AudioClip>(SoundPath + name);
        if (clip == null)
        {
            Debug.LogWarningFormat("PlaySound: {0} not existed", name);
            return;
        }
        soundAudioSource.PlayOneShot(clip);
    }
    protected void PlayClipOnAudioSource(AudioSource source,AudioClip clip,bool isLoop)
    {

    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UISystemConfig : UIWindow
{
    public Image musicOff;
    public Image soundOff;
    public Toggle toggleMusic;
    public Toggle toggleSound;
    public Slider sliderMusic;
    public Slider sliderSound;

    void Start()
    {
        this.toggleMusic.isOn = Config.MusicOn;
        this.toggleSound.isOn = Config.SoundOn;
        this.sliderMusic.value = Config.MusicVolume;
        this.sliderSound.value = Config.SoundVolume;
    }

    public override void OnYesClick()
    {
        SoundManager.Instance.PlaySound(SoundDefine.SFX_UI_Click);
        PlayerPrefs.Save();
        base.OnYesClick();
    }

    public void MusicToogle()
    {
        bool on = toggleMusic.isOn;
        musicOff.enabled = !on;
        Config.MusicOn = on;
        SoundManager.Instance.PlaySound(SoundDefine.SFX_UI_Click);
    }
    public void SoundToogle()
    {
        bool on = toggleSound.isOn;
        soundOff.enabled = !on;
        Config.SoundOn = on;
        SoundManager.Instance.PlaySound(SoundDefine.SFX_UI_Click);
    }

    public void MusicVolume()
    {
        float vol = sliderMusic.value;
        Config.MusicVolume = (int)vol;
        PlaySound();
    }
    public void SoundVolume()
    {
        float vol = sliderSound.value;
        Config.SoundVolume = (int)vol;
        PlaySound();
    }
    float lastPlay = 0;
    private void PlaySound()
    {
        if (Time.realtimeSinceStartup - lastPlay > 0.1)
        {
            lastPlay = Time.realtimeSinceStartup;
            SoundManager.Instance.PlaySound(SoundDefine.SFX_UI_Click);
        }
    }
}

Config:用于保存游戏数据在本地

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;


class Config
{
    public static bool MusicOn
    {
        get { return PlayerPrefs.GetInt("Music", 1) == 1; }
        set
        {
            PlayerPrefs.SetInt("Music", value ? 1 : 0);
            SoundManager.Instance.MusicOn = value;
        }
    }
    public static bool SoundOn
    {
        get { return PlayerPrefs.GetInt("Sound", 1) == 1; }
        set
        {
            PlayerPrefs.SetInt("Sound", value ? 1 : 0);
            SoundManager.Instance.SoundOn = value;
        }
    }

    public static int MusicVolume
    {
        get { return PlayerPrefs.GetInt("MusicVolume", 100); }
        set
        {
            PlayerPrefs.SetInt("MusicVolume", value);
            SoundManager.Instance.MusicVolume = value;
        }
    }
    public static int SoundVolume
    {
        get { return PlayerPrefs.GetInt("SoundVolume", 100); }
        set
        {
            PlayerPrefs.SetInt("SoundVolume", value);
            SoundManager.Instance.SoundVolume = value;
        }
    }

    ~Config()
    {
        PlayerPrefs.Save();
    }
}

寻路系统

分析

寻路组件运用之前,先要对环境做烘焙,将不动的物体设静态

代码

PlayerInputController新增

private NavMeshAgent agent;
private bool autoNav = false;
int NavNpcId = 0;

public void StartNav(Vector3 target,int npcId)
{
    StartCoroutine(BeginNav(target));
    this.NavNpcId = npcId;
}
IEnumerator BeginNav(Vector3 target)
{
    agent.SetDestination(target);
    yield return null;
    autoNav = true;
    if (state != CharacterState.Move)
    {
        state = CharacterState.Move;
        this.character.MoveForward(); // 设置速度
        this.SendEntityEvent(EntityEvent.MoveFwd);
        agent.speed = this.character.speed / 100f;
    }
}
public void StopNav()
{
    autoNav = false;
    NavNpcId = 0;
    agent.ResetPath();
    if (state != CharacterState.Idle)
    {
        state = CharacterState.Idle;
        this.rb.velocity = Vector3.zero;
        this.character.Stop();
        this.SendEntityEvent(EntityEvent.Idle);
    }
    // 关闭可视化路径
    NavPathRenderer.Instance.SetPath(null, Vector3.zero);
}
public void NavMove()
{
    if (agent.pathPending) return; // 寻路完成后pathPending为true
    if (agent.pathStatus == NavMeshPathStatus.PathInvalid)
    {
        StopNav();
        return;
    }
    if (agent.pathStatus != NavMeshPathStatus.PathComplete) return; // 和第一种用法一样
    if (Mathf.Abs(Input.GetAxis("Vertical")) > 0.1 || Mathf.Abs(Input.GetAxis("Horizontal")) > 0.1)
    {
        StopNav();
        return;
    }
    // 设置可视化的路径
    NavPathRenderer.Instance.SetPath(agent.path, agent.destination);
    if (agent.isStopped || agent.remainingDistance < GameDefine.NavDistanceToTarget)
    {
        NPCManager.Instance.GetNpcController(NavNpcId).Interactive();
        StopNav();
        return;
    }
}

private void LateUpdate()
{
    if (this.character == null) return;

    Vector3 offset = this.rb.transform.position - lastPos;
    this.speed = (int)(offset.magnitude * 100f / Time.deltaTime);
    //Debug.LogFormat("LateUpdate velocity {0} : {1}", this.rb.velocity.magnitude, this.speed);
    this.lastPos = this.rb.transform.position;
    // 角色和刚体位置相差太远时作修正,并同步给服务器和其他人
    if ((GameObjectTool.WorldToLogic(this.rb.transform.position) - this.character.position).magnitude > 50)
    {
        this.character.SetPosition(GameObjectTool.WorldToLogic(this.rb.transform.position));
        this.SendEntityEvent(EntityEvent.None);
    }
    this.transform.position = this.rb.transform.position;
    // 修正角度,并同步给其他人
    Vector3 dir = GameObjectTool.LogicToWorld(character.direction);
    Quaternion rot = new Quaternion();
    rot.SetFromToRotation(dir, this.transform.forward);
    if (rot.eulerAngles.y > this.turnAngle && rot.eulerAngles.y < (360 - this.turnAngle))
    {
        character.SetDirection(GameObjectTool.WorldToLogic(this.transform.forward));
        this.SendEntityEvent(EntityEvent.None);
    }
}

NPCController新增

private void OnMouseDown()
{
    if (Vector3.Distance(this.transform.position, User.Instance.CurrentCharacterObject.transform.position) > GameDefine.NavDistanceToTarget)
        User.Instance.CurrentCharacterObject.StartNav(this.transform.position,this.npcID);
    else
        Interactive();
}

关于服务器校验

[MenuItem("Map Tools/Generate NavData")]
   public static void GenerateNavData()
   {
       Material red = new Material(Shader.Find("Particles/Alpha Blended"));
       red.color = Color.red;
       red.SetColor("_TintColor", Color.red);
       red.enableInstancing = true;
       GameObject go = GameObject.Find("MinimapBoundingBox");
       if (go != null)
       {
           GameObject root = new GameObject("Root");
           BoxCollider bound = go.GetComponent<BoxCollider>();
           float step = 1f;
           for (float x = bound.bounds.min.x; x < bound.bounds.max.x; x += step)
           {
               for (float z = bound.bounds.min.z; z < bound.bounds.max.z; z += step)
               {
                   for (float y = bound.bounds.min.y; y < bound.bounds.max.y + 5f; y += step)
                   {
                       var pos = new Vector3(x, y, z);
                       NavMeshHit hit;
                       if (NavMesh.SamplePosition(pos,out hit,0.5f,NavMesh.AllAreas)) // 关键代码,位置采样,只要半径0.5米内有导航区域返回true
                       {
                           if (hit.hit)
                           {
                               // 客户端为了直观创建立方体,实际上要用到服务器
                               // 这里服务器只需要生成一个三维数组的配置表,有方块的为1,其余为0即可,可以写成[x][y][z] = 1
                               var box = GameObject.CreatePrimitive(PrimitiveType.Cube); 
                               box.name = "Hit" + hit.mask;
                               box.GetComponent<MeshRenderer>().sharedMaterial = red;
                               box.transform.SetParent(root.transform,true);
                               box.transform.position = pos;
                               box.transform.localScale = Vector3.one * 0.9f;
                           }
                       }
                   }
               }
           }
       }
   }

生成这个数组数据后,变成配置文件传给服务器,就能判断当前角色是否穿墙,防止作弊

问题

  1. 看到别的玩家撞墙一卡一卡的
  2. 重复点击进入游戏会生成多个角色
  3. 移动状态不太对

文章作者: 微笑紫瞳星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 微笑紫瞳星 !
评论
  目录