基础系统
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系统


添加配置表
- 在Data/Table目录下新建一个配置表,填好对应的信息

重新生成txt文件,并往客户端和服务器都复制一份
在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;
}
}
}
道具系统
需求分析

道具系统的对外接口:

设计图:

数据结构添加

- 在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; }
}
}
编写和更改协议,加上NItemInfo的Message,并将这个message添加到NCharacterInfo这个message下,并将NCharacterInfo添加到UserGameEnterResponse下,并用工具重新生成协议
更改第三个数据结构:数据库。新增一个表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代码(待添加)
商店系统
需求分析




添加数据
- 配置表新增ShopDefine和ShopItemDefine
- 协议新增NcharacterInfo的gold,还添加新协议ItemBuyRequest和ItemBuyResponse。
- 数据库新加金币字段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;
}
- 在服务器和客户端的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();
}
商店系统制作
准备工作:
- 建立商店的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编辑
使用:
- 在Group中对着Master点右键就能创建通道,在Master的子节点下
- 每一个AudioSource除了AudioClip之外,就能指定一个Output,放置的就是Audio Mixer Group,通过操作Output我们可以决定在哪一个混音器通道做输出
- 在Inspector中右键混音器通道法的一个属性可以添加Exposed Parameters,在混音器界面的右上角可以查看,这样可以通过代码访问
- 混音器面板上,我们可以设置快照,例如我们每个场景中音乐通道和音效通道的音量大小不一样,我们可以设置快照进行切换,这种切换是平滑的
代码
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;
}
}
}
}
}
}
}
生成这个数组数据后,变成配置文件传给服务器,就能判断当前角色是否穿墙,防止作弊
问题
- 看到别的玩家撞墙一卡一卡的
- 重复点击进入游戏会生成多个角色
- 移动状态不太对