Unity游戏开发终极实战之MMORPG


知识点一览

开发准备

软件安装:

  1. 下载安装Unity,不用下最新版本,稳定的版本即可,如2018.4.16c1
  2. 下载安装Visual Studio 2017,安装是必须勾选Unity选项
  3. 打开Unity编辑器,在Edit->Preference->External Tools中的External Script Editor中选择你刚刚安装的VS版本。勾选Editor Attaching。
  4. 下载安装MS SQL Server。
  5. 下载安装SSMS(SQL Server Management Studio)
  6. 下载安装git
  7. 工具:Beyond Compare

创建目录:

除了版本管理默认的.git和README.md文件外,我们需要规划好文件存放的地方,便于管理,需要创建的文件夹有:
Art:美术资源
Doc:文档
Src:源码
Tools:工具,例如数据表格处理工具,表格转换工具,协议转换工具

用Git做版本管理:

项目准备:

文档:最关键的是策划文档,例如MMORPG有一个总案,介绍世界观,美术风格,核心玩法等。文档包括策划文档、美术文档、技术文档
工具:配置表工具、协议工具、其他工具
框架:依赖文档做需求分析,搭建框架,客户端包括:日志,配置,数据加载,基础组件,服务端多一个数据库访问
协议:客户端和服务端约定的规则,采用的是Protobuf,可以创建协议自动生成代码

用git下载项目资源到本地:

文件夹中右键菜单->git Clone->填写URL位置默认为当前文件夹->点击OK

框架设计

其实在在真正的项目当中,编程所花的时间是非常非常少的,真正需要花时间的部分在于设计,有时候设计花的时间远远超过编程的时间。它需要你从需求分析开始,了解各种条件,再去做技术方案,每个点可能都需要技术方案的预先研究。
设计流程:明确设计目标->充分考虑设计方案->可行性评估->选择最优方案
设计内容:游戏玩法设计->整体架构设计及方案选择->客户端框架设计->服务端框架设计->工作流程设计
设计的阶段:整体框架的设计->系统方案的设计->模块详细设计->迭代优化

框架分层:

层是自己分出来的,除了基础服务层,其他层都和业务需求息息相关,不存在一个通用的框架。在代码层面,层就是一个文件夹,里面存放着以这个层名为结尾的类。不同的人有不同的分层方法,形成不同框架。

底层基础支撑

网络消息处理

通讯组件

  • NetworkClient & TcpSocketListener
  • 封包处理器 PackageHandler
  • 信息分发器 MessageDistributer
  • 消息分配处理 MessageDispatch

注意:对这一章的内容,初学者不需要理解,只需要使用,所以看不懂代码没关系,留下一个概念即可,可直接跳下一章进行学习

客户端:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using UnityEngine;
using SkillBridge.Message;

客户端和服务端通讯的第一件事,连接到服务端:

namespace Network
{
    class NetClient : MonoSingleton<NetClient>
    {
      --snip--
      public void Connect(int times = DEF_TRY_CONNECT_TIMES)
      {
          if (this.connecting)
          {
              return;
          }

          if (this.clientSocket != null)
          {
              this.clientSocket.Close();
          }
          if (this.address == default(IPEndPoint))
          {
              throw new Exception("Please Init first.");
          }
          Debug.Log("DoConnect");
          this.connecting = true;
          this.lastSendTime = 0;
          // 真正的链接方法
          this.DoConnect();
      }
      --snip--
      // 真正去链接
      void DoConnect()
      {
          Debug.Log("NetClient.DoConnect on " + this.address.ToString());
          try
          {
              if (this.clientSocket != null)
              {
                  this.clientSocket.Close();
              }


              this.clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
              this.clientSocket.Blocking = true;                      // 阻塞的方式,可以随时接收状态,阻塞的意思是必须请求要有返回信息才进行下一步

              Debug.Log(string.Format("Connect[{0}] to server {1}", this.retryTimes, this.address) + "\n");
              IAsyncResult result = this.clientSocket.BeginConnect(this.address, null, null);          // 异步的方法
              bool success = result.AsyncWaitHandle.WaitOne(NetConnectTimeout);                        // 等待一个异步时间
              if (success)
              {
                  this.clientSocket.EndConnect(result);              //链接成功,结束异步请求 
              }
          }
          catch(SocketException ex)                                 // 捕获异常
          {
              if(ex.SocketErrorCode == SocketError.ConnectionRefused)
              {
                  this.CloseConnection(NET_ERROR_FAIL_TO_CONNECT);
              }
              Debug.LogErrorFormat("DoConnect SocketException:[{0},{1},{2}]{3} ", ex.ErrorCode,ex.SocketErrorCode,ex.NativeErrorCode, ex.ToString()); 
          }
          catch (Exception e)
          {
              Debug.Log("DoConnect Exception:" + e.ToString() + "\n");
          }

          if (this.clientSocket.Connected)                       // 一旦链接上,采用非阻塞的进行通讯,否则会在网络差的时候会使整个程序卡住
          {
              this.clientSocket.Blocking = false;
              this.RaiseConnected(0, "Success");
          }
          else
          {
              this.retryTimes++;
              if (this.retryTimes >= this.retryTimesTotal)
              {
                  this.RaiseConnected(1, "Cannot connect to server");
              }
          }
          this.connecting = false;
      }
      --snip--
    }
}

以下方法都在NetClient类中:
发送消息:

public void SendMessage(NetMessage message)   // NetMessage是Protobuf生成的东西
{
    if (!running)
    {
        return;
    }

    if (!this.Connected)
    {
        this.receiveBuffer.Position = 0;
        this.sendBuffer.Position = sendOffset = 0;

        this.Connect();
        Debug.Log("Connect Server before Send Message!");
        return;
    }

    sendQueue.Enqueue(message);           // 进入队列,利于依次发送,在Update时发送

    if (this.lastSendTime == 0)
    {
        this.lastSendTime = Time.time;
    }
}

public void Update()
{
    if (!running)
    {
        return;
    }

    if (this.KeepConnect())              //保证断线重连
    {
        if (this.ProcessRecv())          // 提取信息
        {
            if (this.Connected)
            {
                this.ProcessSend();
                this.ProceeMessage();   // 消息分发器分发
            }
        }
    }
}

服务端:

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

namespace Network
{
  /// <summary>
  /// Listens for socket connection on a given address and port.
  /// </summary>
  public class TcpSocketListener : IDisposable
  {
    --snip--
    public void Start()
    {
      lock (this)
      {
          if (!IsRunning)
          {
              listenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
              listenerSocket.Bind(endPoint);    // 和客户端的链接不同,这里绑定在一个端口上
              listenerSocket.Listen(connectionBacklog); // 并进行监听
              BeginAccept(args);
          }
          else
              throw new InvalidOperationException("The Server is already running.");
      }

    }
  }
  --snip--
}
// 链接过来一个客户端,接受并开始通讯
private void BeginAccept(SocketAsyncEventArgs args)
{
    args.AcceptSocket = null;
    listenerSocket.AcceptAsync(args);
    /*listenerSocket.InvokeAsyncMethod(new SocketAsyncMethod(listenerSocket.AcceptAsync)
        , OnSocketAccepted, args);*/
}
// 接受链接后的处理
private void OnSocketAccepted(object sender, SocketAsyncEventArgs e)
{
    SocketError error = e.SocketError;
    if (e.SocketError == SocketError.OperationAborted)
        return; //Server was stopped

    if (e.SocketError == SocketError.Success)
    {
        Socket handler = e.AcceptSocket;     // 传进来的客户端
        OnSocketConnected(handler);
    }

    lock (this)
    {
        BeginAccept(e);
    }
}

主入口:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SkillBridge.Message;
using ProtoBuf;
using System.IO;
using Common;
using System.Threading;

namespace GameServer
{
    class Program
    {
        static void Main(string[] args)
        {
            FileInfo fi = new System.IO.FileInfo("log4net.xml");
            log4net.Config.XmlConfigurator.ConfigureAndWatch(fi);
            Log.Init("GameServer");
            Log.Info("Game Server Init");

            GameServer server = new GameServer();
            server.Init();
            server.Start();
            Console.WriteLine("Game Server Running......");
            CommandHelper.Run();
            Log.Info("Game Server Exiting...");
            server.Stop();
            Log.Info("Game Server Exited");
        }
    }
}

GameServer类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using GameServer.Network;
using System.Configuration;

using System.Threading;

using Network;
using GameServer.Services;
using GameServer.Managers;
namespace GameServer
{
    class GameServer
    {
        NetService network;
        Thread thread;
        bool running = false;
        public bool Init()
        {
            int Port = Properties.Settings.Default.ServerPort;
            network = new NetService();            // 重要的类
            network.Init(Port);
            DBService.Instance.Init();
            DataManager.Instance.Load();
            MapService.Instance.Init();
            UserService.Instance.Init();
            ItemService.Instance.Init();
            QuestService.Instance.Init();
            FriendService.Instance.Init();
            TeamService.Instance.Init();
            GuildService.Instance.Init();
            ChatService.Instance.Init();
            thread = new Thread(new ThreadStart(this.Update));

            return true;
        }

        public void Start()
        {
            network.Start();
            running = true;
            thread.Start();
        }


        public void Stop()
        {
            running = false;
            thread.Join();
            network.Stop();
        }

        public void Update()
        {
            var mapManager = MapManager.Instance;
            while (running)
            {
                Time.Tick();
                Thread.Sleep(100);
                //Console.WriteLine("{0} {1} {2} {3} {4}", Time.deltaTime, Time.frameCount, Time.ticks, Time.time, Time.realtimeSinceStartup);
                mapManager.Update();
            }
        }
    }
}

NetService类:
(重要,MMO底层)

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

using System.Net;
using System.Net.Sockets;
using GameServer;
using Common;

namespace Network
{
    class NetService
    {
        static TcpSocketListener ServerListener;
        public bool Init(int port)
        {
            ServerListener = new TcpSocketListener("127.0.0.1", GameServer.Properties.Settings.Default.ServerPort, 10); // 创建监听器
            ServerListener.SocketConnected += OnSocketConnected;
            return true;
        }


        public void Start()
        {
            //启动监听
            Log.Warning("Starting Listener...");
            ServerListener.Start();

            MessageDistributer<NetConnection<NetSession>>.Instance.Start(8);
            Log.Warning("NetService Started");
        }


        public void Stop()
        {
            Log.Warning("Stop NetService...");

            ServerListener.Stop();

            Log.Warning("Stoping Message Handler...");
            MessageDistributer<NetConnection<NetSession>>.Instance.Stop();
        }

        private void OnSocketConnected(object sender, Socket e)
        {
            IPEndPoint clientIP = (IPEndPoint)e.RemoteEndPoint;
            //可以在这里对IP做一级验证,比如黑名单

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            NetSession session = new NetSession();

            NetConnection<NetSession> connection = new NetConnection<NetSession>(e, args,
                new NetConnection<NetSession>.DataReceivedCallback(DataReceived),
                new NetConnection<NetSession>.DisconnectedCallback(Disconnected), session);


            Log.WarningFormat("Client[{0}]] Connected", clientIP);
        }


        /// <summary>
        /// 连接断开回调
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        static void Disconnected(NetConnection<NetSession> sender, SocketAsyncEventArgs e)
        {
            //Performance.ServerConnect = Interlocked.Decrement(ref Performance.ServerConnect);
            sender.Session.Disconnected();
            Log.WarningFormat("Client[{0}] Disconnected", e.RemoteEndPoint);
        }


        /// <summary>
        /// 接受数据回调
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        static void DataReceived(NetConnection<NetSession> sender, DataEventArgs e)
        {
            Log.WarningFormat("Client[{0}] DataReceived Len:{1}", e.RemoteEndPoint, e.Length);
            //由包处理器处理封包
            lock (sender.packageHandler)
            {
                sender.packageHandler.ReceiveData(e.Data, 0, e.Data.Length);
            }
            //PacketsPerSec = Interlocked.Increment(ref PacketsPerSec);
            //RecvBytesPerSec = Interlocked.Add(ref RecvBytesPerSec, e.Data.Length);
        }
    }
}

简单小案例:

客户端:

using UntiyEngine;

public class Login:MonoBehaviour{
  void Start(){
    Vetwork.NetClient.Instance.Init("127.0.0.1",8000);
    Network.NetClient.Instance.Connect();

    SkillBridge.Message.NetMessage msg = new SkillBridge.Message.NetMessage();
    msg.Request = new SkillBridge.Message.NewMessageRequest();
    msg.Request.firstRequest = new SkillBridge.Message.FirstTestRequest();
    msg.Request.firstRequest.Helloworld = "Hello World";
    Network.NetClient.Instance.SendMessage(msg);
  }
}

服务端:

namespace GameServer.Services{
    void Init(){

    }

    public void Start(){
      MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<FirstTestRequest>(this.OnFirstTestRequest);
    }

    void OnFirstTestRequest(NetConnection<NetSessin> sender,FirstTestRequest request){
      Log.InfoFormat("OnFirstTestRequest:Helloworld:{0}",request.Helloworld);
    }

}

此外,还要在message.proto里加上新协议,并用Protobuf重新生成协议,然后在消息分发器里加新消息,并且在GameServer里启动这个服务器。

基础场景与UI

进度条制作

UI九宫格:一张贴图会被划分为九个区域,当贴图和区域大小不匹配时,中间被拉伸,四角保持不变,而上下左右都会做水平或垂直拉伸。这时图片类型为Sliced。
做法:直接把中间的区域框在需要拉伸的地方
优点:节省资源,长图能用短图代替

制作字体

工程栏中 Create->Custon Font创建自定义字体,再创建字体的材质(采用提供的数字图片),材质的Shader改成UI->Unit->Detail,然后把材质绑定到字体上,字体属性:

Character Rects:
    Size:有多少个字符
    Element:
        Index:填字符的ASCII码
这部分内容查一下(Unity自定义字体)博客照做即可
可以采用BM Font工具来调

在UI建立Text组件的时候,就能在Font属性和Material中选择自己的字体和材质了。
增加效果:
在字体UI下增加Shadow组件增加阴影,Outline组件可以描边

用户注册与登录

游戏系统框架图

客户端:

进入界面:

LoadingManager:

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

using SkillBridge.Message;
using ProtoBuf;
using Services;

public class LoadingManager : MonoBehaviour {

    public GameObject UITips;
    public GameObject UILoading;
    public GameObject UILogin;

    public Slider progressBar;
    public Text progressText;
    public Text progressNumber;

    // Use this for initialization
    IEnumerator Start()
    {
        // 注意这两行
        // log4net.xml可以定义日志格式
        log4net.Config.XmlConfigurator.ConfigureAndWatch(new System.IO.FileInfo("log4net.xml"));
        UnityLogger.Init();
        Common.Log.Init("Unity");
        Common.Log.Info("LoadingManager start");

        UITips.SetActive(true);
        UILoading.SetActive(false);
        UILogin.SetActive(false);
        yield return new WaitForSeconds(2f);
        UILoading.SetActive(true);
        yield return new WaitForSeconds(1f);
        UITips.SetActive(false);

        yield return DataManager.Instance.LoadData();

        //Init basic services
        //MapService.Instance.Init();
        UserService.Instance.Init();


        // Fake Loading Simulate
        for (float i = 50; i < 100;)
        {
            i += Random.Range(0.1f, 1.5f);
            progressBar.value = i;
            yield return new WaitForEndOfFrame();
        }

        UILoading.SetActive(false);
        UILogin.SetActive(true);
        yield return null;
    }


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

    }
}

UnityLogger:

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

public static class UnityLogger
{

    public static void Init()
    {
        Application.logMessageReceived += onLogMessageReceived;
        Common.Log.Init("Unity");
    }

    private static ILog log = LogManager.GetLogger("Unity");

    private static void onLogMessageReceived(string condition, string stackTrace, UnityEngine.LogType type)
    {
        switch(type)
        {
            case LogType.Error:
                log.ErrorFormat("{0}\r\n{1}", condition, stackTrace.Replace("\n", "\r\n"));
                break;
            case LogType.Assert:
                log.DebugFormat("{0}\r\n{1}", condition, stackTrace.Replace("\n", "\r\n"));
                break;
            case LogType.Exception:
                log.FatalFormat("{0\r\n{1}", condition, stackTrace.Replace("\n", "\r\n"));
                break;
            case LogType.Warning:
                log.WarnFormat("{0}\r\n{1}", condition, stackTrace.Replace("\n", "\r\n"));
                break;
            default:
                log.Info(condition);
                break;
        }
    }
}

用户注册

注册逻辑:
UIRegister:

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

public class UIRegister : MonoBehaviour {


    public InputField username;
    public InputField password;
    public InputField passwordConfirm;
    public Button buttonRegister;

    public GameObject uiLogin;
    // Use this for initialization
    void Start () {
        UserService.Instance.OnRegister = OnRegister;
    }

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

    public void OnClickRegister()
    {
        if (string.IsNullOrEmpty(this.username.text))
        {
            MessageBox.Show("请输入账号");
            return;
        }
        if (string.IsNullOrEmpty(this.password.text))
        {
            MessageBox.Show("请输入密码");
            return;
        }
        if (string.IsNullOrEmpty(this.passwordConfirm.text))
        {
            MessageBox.Show("请输入确认密码");
            return;
        }
        if (this.password.text != this.passwordConfirm.text)
        {
            MessageBox.Show("两次输入的密码不一致");
            return;
        }

        UserService.Instance.SendRegister(this.username.text,this.password.text);
    }

    void OnRegister(Result result, string message)
    {
        if (result == Result.Success)
        {
            //登录成功,进入角色选择
            MessageBox.Show("注册成功,请登录", "提示", MessageBoxType.Information).OnYes = this.CloseRegister;
        }
        else
            MessageBox.Show(message, "错误", MessageBoxType.Error);
    }

    void CloseRegister()
    {
        this.gameObject.SetActive(false);
        uiLogin.SetActive(true);
    }
}

用户登录:

UILogin:

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

public class UILogin : MonoBehaviour {
    public InputField username;
    public InputField password;
    public Button buttonLogin;
    public Button buttonRegister;

    // Use this for initialization
    void Start () {
        UserService.Instance.OnLogin = OnLogin;
    }
    // Update is called once per frame
    void Update () {
		
	}
    // 注册成功就发送信息,否则弹出错误的消息框
    public void OnClickLogin()
    {
        if (string.IsNullOrEmpty(this.username.text))
        {
            MessageBox.Show("请输入账号");
            return;
        }
        if (string.IsNullOrEmpty(this.password.text))
        {
            MessageBox.Show("请输入密码");
            return;
        }
        // Enter Game
        UserService.Instance.SendLogin(this.username.text,this.password.text);

    }

    void OnLogin(Result result, string message)
    {
        if (result == Result.Success)
        {
            //登录成功,进入角色选择
            //MessageBox.Show("登录成功,准备角色选择" + message,"提示", MessageBoxType.Information);
            SceneManager.Instance.LoadScene("CharSelect");

        }
        else
            MessageBox.Show(message, "错误", MessageBoxType.Error);
    }
}

消息框:

MessageBox:

using UnityEngine;

class MessageBox
{
    static Object cacheObject = null;

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

        GameObject go = (GameObject)GameObject.Instantiate(cacheObject);
        UIMessageBox msgbox = go.GetComponent<UIMessageBox>();
        msgbox.Init(title, message, type, btnOK, btnCancel);
        return msgbox;
    }
}

public enum MessageBoxType
{
    /// <summary>
    /// Information Dialog with OK button
    /// </summary>
    Information = 1,

    /// <summary>
    /// Confirm Dialog whit OK and Cancel buttons
    /// </summary>
    Confirm = 2,

    /// <summary>
    /// Error Dialog with OK buttons
    /// </summary>
    Error = 3
}

UIMessageBox:

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class UIMessageBox : MonoBehaviour {

    public Text title;
    public Text message;
    public Image[] icons;
    public Button buttonYes;
    public Button buttonNo;
    public Button buttonClose;

    public Text buttonYesTitle;
    public Text buttonNoTitle;

    public UnityAction OnYes;
    public UnityAction OnNo;
    

    // Use this for initialization
    void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
		
	}

    public void Init(string title, string message, MessageBoxType type = MessageBoxType.Information, string btnOK = "", string btnCancel = "")
    {
        if (!string.IsNullOrEmpty(title)) this.title.text = title;
        this.message.text = message;
        this.icons[0].enabled = type == MessageBoxType.Information;
        this.icons[1].enabled = type == MessageBoxType.Confirm;
        this.icons[2].enabled = type == MessageBoxType.Error;

        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);

        this.buttonNo.gameObject.SetActive(type == MessageBoxType.Confirm);
    }

    void OnClickYes()
    {
        Destroy(this.gameObject);
        if (this.OnYes != null)
            this.OnYes();
    }

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

消息传输(Service层)

当客户端发送数据之后,数据交由Service层处理,如下面两行代码:

UserService.Instance.SendRegister(this.username.text,this.password.text);
UserService.Instance.SendLogin(this.username.text,this.password.text);

下面就需要关注UserService这个类了,这个类被划分在Service层:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common;
using Network;
using UnityEngine;

using SkillBridge.Message;

namespace Services
{
    class UserService : Singleton<UserService>, IDisposable
    {

        public UnityEngine.Events.UnityAction<Result, string> OnLogin;
        public UnityEngine.Events.UnityAction<Result, string> OnRegister;
        NetMessage pendingMessage = null;
        bool connected = false;
        
        public UserService()
        {
            // 这个类监听了链接和断开的事件
            NetClient.Instance.OnConnect += OnGameServerConnect;
            NetClient.Instance.OnDisconnect += OnGameServerDisconnect;
            // 这里订阅了用户注册和登录的响应
            MessageDistributer.Instance.Subscribe<UserLoginResponse>(this.OnUserLogin);
            MessageDistributer.Instance.Subscribe<UserRegisterResponse>(this.OnUserRegister);
            
        }

        public void Dispose()
        {
            MessageDistributer.Instance.Unsubscribe<UserLoginResponse>(this.OnUserLogin);
            MessageDistributer.Instance.Unsubscribe<UserRegisterResponse>(this.OnUserRegister);
            NetClient.Instance.OnConnect -= OnGameServerConnect;
            NetClient.Instance.OnDisconnect -= OnGameServerDisconnect;
        }

        public void Init()
        {

        }
        // 这里链接到服务器,现在写死是本地
        public void ConnectToServer()
        {
            Debug.Log("ConnectToServer() Start ");
            //NetClient.Instance.CryptKey = this.SessionId;
            NetClient.Instance.Init("127.0.0.1", 8000);
            NetClient.Instance.Connect();
        }

        // 链接时判断是否有连接前需要补发的消息,并判断网络是否断开
        void OnGameServerConnect(int result, string reason)
        {
            Log.InfoFormat("LoadingMesager::OnGameServerConnect :{0} reason:{1}", result, reason);
            if (NetClient.Instance.Connected)
            {
                this.connected = true;
                if(this.pendingMessage!=null)
                {
                    NetClient.Instance.SendMessage(this.pendingMessage);
                    this.pendingMessage = null;
                }
            }
            else
            {
                if (!this.DisconnectNotify(result, reason))
                {
                    MessageBox.Show(string.Format("网络错误,无法连接到服务器!\n RESULT:{0} ERROR:{1}", result, reason), "错误", MessageBoxType.Error);
                }
            }
        }
        // 服务器断开时的处理,这里发送了一个通知
        public void OnGameServerDisconnect(int result, string reason)
        {
            this.DisconnectNotify(result, reason);
            return;
        }

        bool DisconnectNotify(int result,string reason)
        {
            if (this.pendingMessage != null)
            {
                if (this.pendingMessage.Request.userLogin!=null)
                {
                    if (this.OnLogin != null)
                    {
                        this.OnLogin(Result.Failed, string.Format("服务器断开!\n RESULT:{0} ERROR:{1}", result, reason));
                    }
                }
                else if(this.pendingMessage.Request.userRegister!=null)
                {
                    if (this.OnRegister != null)
                    {
                        this.OnRegister(Result.Failed, string.Format("服务器断开!\n RESULT:{0} ERROR:{1}", result, reason));
                    }
                }
                return true;
            }
            return false;
        }
        // 这里和用户注册同理
        public void SendLogin(string user, string psw)
        {
            Debug.LogFormat("UserLoginRequest::user :{0} psw:{1}", user, psw);
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.userLogin = new UserLoginRequest();
            message.Request.userLogin.User = user;
            message.Request.userLogin.Passward = psw;

            if (this.connected && NetClient.Instance.Connected)
            {
                this.pendingMessage = null;
                NetClient.Instance.SendMessage(message);
            }
            else
            {
                this.pendingMessage = message;
                this.ConnectToServer();
            }
        }

        void OnUserLogin(object sender, UserLoginResponse response)
        {
            Debug.LogFormat("OnLogin:{0} [{1}]", response.Result, response.Errormsg);

            if (response.Result == Result.Success)
            {//登陆成功逻辑,这里把服务器返回的信息记录到本地
                Models.User.Instance.SetupUserInfo(response.Userinfo);
            };
            if (this.OnLogin != null)
            {
                this.OnLogin(response.Result, response.Errormsg);

            }
        }


        public void SendRegister(string user, string psw)
        {
            Debug.LogFormat("UserRegisterRequest::user :{0} psw:{1}", user, psw);
            // 生成以包含用户名和密码的请求
            NetMessage message = new NetMessage();
            message.Request = new NetMessageRequest();
            message.Request.userRegister = new UserRegisterRequest();
            message.Request.userRegister.User = user;
            message.Request.userRegister.Passward = psw;
            // 判断是否链接上,连上了发送消息,否则掉线重连后再通过队列发送信息
            if (this.connected && NetClient.Instance.Connected)
            {
                this.pendingMessage = null;
                NetClient.Instance.SendMessage(message);
            }
            else
            {
                this.pendingMessage = message;
                this.ConnectToServer();
            }
        }
        // 这里定义了客户端收到服务器发回来的信息后的响应,回馈到UI层
        void OnUserRegister(object sender, UserRegisterResponse response)
        {
            Debug.LogFormat("OnUserRegister:{0} [{1}]", response.Result, response.Errormsg);

            if (this.OnRegister != null)
            {
                this.OnRegister(response.Result, response.Errormsg);
            }
        }
    }
}

服务端:

这里只列举一种情况(注册),登录同理:

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

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

        public UserService()
        {
            // 订阅信息,只有有登录数据过来,则执行OnRegister方法
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<UserRegisterRequest>(this.OnRegister);
            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<UserLoginRequest>(this.OnLogin);
        }

        public void Init()
        {

        }

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

            NetMessage message = new NetMessage();
            message.Response = new NetMessageResponse();
            message.Response.userRegister = new UserRegisterResponse();
            // 查询数据库是否有这个用户
            TUser user = DBService.Instance.Entities.Users.Where(u => u.Username == request.User).FirstOrDefault();
            if (user != null)
            {
                message.Response.userRegister.Result = Result.Failed;
                message.Response.userRegister.Errormsg = "用户已存在.";
            }
            else
            {
                // 先添加一个player,在添加一个user
                TPlayer player = DBService.Instance.Entities.Players.Add(new TPlayer());
                DBService.Instance.Entities.Users.Add(new TUser() { Username = request.User, Password = request.Passward, Player = player });
                DBService.Instance.Entities.SaveChanges();
                message.Response.userRegister.Result = Result.Success;
                message.Response.userRegister.Errormsg = "None";
            }

            byte[] data = PackageHandler.PackMessage(message);
            sender.SendData(data, 0, data.Length);
        }
        void OnLogin(NetConnection<NetSession> sender, UserLoginRequest request)
        {
            Log.InfoFormat("UserRegisterRequest: User:{0}  Pass:{1}", request.User, request.Passward);

            NetMessage message = new NetMessage();
            message.Response = new NetMessageResponse();
            message.Response.userRegister = new UserRegisterResponse();

            TUser user = DBService.Instance.Entities.Users.Where(u => u.Username == request.User).FirstOrDefault();
            if (user == null)
            {
                message.Response.userLogin.Result = Result.Failed;
                message.Response.userLogin.Errormsg = "用户不存在";
            }
            else if (request.Passward != user.Password)
            {
                message.Response.userLogin.Result = Result.Failed;
                message.Response.userLogin.Errormsg = "密码不正确.";
            }
            else
            {
                message.Response.userLogin.Result = Result.Success;
                message.Response.userRegister.Errormsg = "None";
                sender.Session.User = user;
                message.Response.userLogin.Userinfo.Id = 1;
                message.Response.userLogin.Userinfo.Player = new NPlayerInfo();
                message.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.Class = (CharacterClass)c.Class;
                    message.Response.userLogin.Userinfo.Player.Characters.Add(info);
                }
            }
            byte[] data = PackageHandler.PackMessage(message);
            sender.SendData(data, 0, data.Length);
        }
    }
}

链接到数据库

要想服务器链接到数据库,必须修改服务端代码中的App.config文件,其中的一段代码:

<connectionStrings><add name="ExtremeWorldEntities" connectionString="metadata=res://*/Entities.csdl|res://*/Entities.ssdl|res://*/Entities.msl;provider=System.Data.SqlClient;provider connection string="
  Data Source=........."" providerName="System.Data.EntityClient" /></connectionStrings>
  <userSettings>
    <GameServer.Properties.Settings>
      <setting name="ServerIP" serializeAs="String">
        <value>127.0.0.1</value>
      </setting>
      <setting name="ServerPort" serializeAs="String">
        <value>8000</value>
      </setting>
    </GameServer.Properties.Settings>
  </userSettings>
</configuration>

把省略号的内容替换成自己的链接字符串即可,这个的查看方式为:
VS菜单栏->工具->链接到数据库->输入服务器名和数据库名称,点击测试->测试成功后点击“高级”,可以看到下面一行是链接字符串
填写正确后,服务器就能正确访问到数据库

角色创建和数据加载

注意:以后新建项目时JsonDotNet要用这个课程的版本,否则容易报错
本节目标:当用户输入正确的用户名密码之后,进入角色选择界面,可以创建和选择已经创建的角色,并显示一定的信息。
客户端收到服务器的回复之后,进入场景的代码是:

SceneManager.Instance.LoadScene("CharSelect");

现在看看SceneManger类:

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

public class SceneManager : MonoSingleton<SceneManager>
{
    UnityAction<float> onProgress = null;

    // Use this for initialization
    protected override void OnStart()
    {
    }

    // Update is called once per frame
    void Update () {
	}
    // 这里开启了一个携程
    public void LoadScene(string name)
    {
        StartCoroutine(LoadLevel(name));
    }

    IEnumerator LoadLevel(string name)
    {
        Debug.LogFormat("LoadLevel: {0}", name);
        // 异步的关卡加载
        AsyncOperation async = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(name);
        async.allowSceneActivation = true;
        async.completed += LevelLoadCompleted;
        while (!async.isDone)
        {
            if (onProgress != null)
                onProgress(async.progress);
            yield return null;
        }
    }

    private void LevelLoadCompleted(AsyncOperation obj)
    {
        if (onProgress != null)
            onProgress(1f);
        Debug.Log("LevelLoadCompleted:" + obj.progress);
    }
}

在进入游戏的时候,我们就需要加载配置表等游戏数据,例如在LoadingManager中:

yield return DataManager.Instance.LoadData();
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Events;
using System.Text;
using System;
using System.IO;

using Common.Data;

using Newtonsoft.Json;

public class DataManager : Singleton<DataManager>
{
    public string DataPath;
    public Dictionary<int, MapDefine> Maps = null;
    public Dictionary<int, CharacterDefine> Characters = null;
    public Dictionary<int, TeleporterDefine> Teleporters = null;
    public Dictionary<int, Dictionary<int, SpawnPointDefine>> SpawnPoints = null;


    public DataManager()
    {
        this.DataPath = "Data/";
        Debug.LogFormat("DataManager > DataManager()");
    }

    public void Load()
    {
        string json = File.ReadAllText(this.DataPath + "MapDefine.txt");
        this.Maps = JsonConvert.DeserializeObject<Dictionary<int, MapDefine>>(json);

        json = File.ReadAllText(this.DataPath + "CharacterDefine.txt");
        this.Characters = JsonConvert.DeserializeObject<Dictionary<int, CharacterDefine>>(json);

        json = File.ReadAllText(this.DataPath + "TeleporterDefine.txt");
        this.Teleporters = JsonConvert.DeserializeObject<Dictionary<int, TeleporterDefine>>(json);

        json = File.ReadAllText(this.DataPath + "SpawnPointDefine.txt");         
        this.SpawnPoints = JsonConvert.DeserializeObject<Dictionary<int, Dictionary<int, SpawnPointDefine>>> (json);
    }

    // 可以看到,这样加载了好几个配置表
    public IEnumerator LoadData()
    {
        string json = File.ReadAllText(this.DataPath + "MapDefine.txt");
        this.Maps = JsonConvert.DeserializeObject<Dictionary<int, MapDefine>>(json);

        yield return null;

        json = File.ReadAllText(this.DataPath + "CharacterDefine.txt");
        this.Characters = JsonConvert.DeserializeObject<Dictionary<int, CharacterDefine>>(json);

        yield return null;

        json = File.ReadAllText(this.DataPath + "TeleporterDefine.txt");
        this.Teleporters = JsonConvert.DeserializeObject<Dictionary<int, TeleporterDefine>>(json);

        yield return null;

        //json = File.ReadAllText(this.DataPath + "SpawnPointDefine.txt");    先注释,否则loading界面会报错
        //this.SpawnPoints = JsonConvert.DeserializeObject<Dictionary<int, Dictionary<int, SpawnPointDefine>>>(json);

        yield return null;
    }

#if UNITY_EDITOR
    public void SaveTeleporters()
    {
        string json = JsonConvert.SerializeObject(this.Teleporters, Formatting.Indented);
        File.WriteAllText(this.DataPath + "TeleporterDefine.txt", json);
    }

    public void SaveSpawnPoints()
    {
        string json = JsonConvert.SerializeObject(this.SpawnPoints, Formatting.Indented);
        File.WriteAllText(this.DataPath + "SpawnPointDefine.txt", json);
    }

#endif
}

配置表长这样:

通过在特定文件夹中填写这样的配置表,我们可以用一个工具来生成对应的json格式的表,用来读取。

如何在选择角色界面对角色作3D展示

首先,在场景中的一个地方设立一个摄像机,把要展示的角色放在摄像机前面。
我们需要创建一个Raw Image,以及一个Render Texture,把Render Texture作为Raw Image的Source Image。图片就会透明。
我们创建一个空物体存放摄像机和角色,方便显示隐藏和管理。摄像机的Tatget Texture设定为我们创建的Render Texture。并把背景从天空盒设置为纯色,可以通过Field of View调节远近。就能把角色显示在UI界面上了。

选择角色并显示对应模型

UICharacterView:

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

public class UICharacterView : MonoBehaviour {

    public GameObject[] characters;


    private int currentCharacter = 0;

    public int CurrectCharacter
    {
        get
        {
            return currentCharacter;
        }
        set
        {
            currentCharacter = value;
            this.UpdateCharacter();
        }
    }

	void Start () {
		
	}
	void Update () {
		
	}
    void UpdateCharacter()
    {
        for(int i=0;i<3;i++)
        {
            characters[i].SetActive(i == this.currentCharacter);
        }
    }
}

创建新角色前端代码

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Models;
using Services;
using SkillBridge.Message;
public class UICharacterSelect : MonoBehaviour {

    public GameObject panelCreate;
    public GameObject panelSelect;

    public GameObject btnCreateCancel;

    public InputField charName;
    CharacterClass charClass;

    public Transform uiCharList;
    public GameObject uiCharInfo;

    public List<GameObject> uiChars = new List<GameObject>();

    public Image[] titles;
    public Text descs;
    public Text[] names;

    private int selectCharacterIdx = -1;

    public UICharacterView characterView;

    // Use this for initialization
    void Start()
    {
        InitCharacterSelect(true);
    }
    public void InitCharacterCreate()
    {
        panelCreate.SetActive(true);
        panelSelect.SetActive(false);
    }

    // 点击按钮响应的事件
    public void OnSelectClass(int charClass)
    {
        this.charClass = (CharacterClass)charClass;
        // 切换模型
        characterView.CurrectCharacter = charClass - 1;
        // 切换标签和文字
        for (int i = 0; i < 3; i++)
        {
            titles[i].gameObject.SetActive(i == charClass - 1);
            names[i].text = DataManager.Instance.Characters[i + 1].Name;
        }

        //descs.text = DataManager.Instance.Characters[charClass].Description;     记得还原这一行
    }

    public void OnSelectCharacter(int idx)
    {
        this.selectCharacterIdx = idx;
        var cha = User.Instance.Info.Player.Characters[idx];
        Debug.LogFormat("Select Char:[{0}]{1}[{2}]", cha.Id, cha.Name, cha.Class);
        User.Instance.CurrentCharacter = cha;
        characterView.CurrectCharacter = idx;
    }
    public void OnClickPlay()
    {
        if (selectCharacterIdx >= 0)
        {
            MessageBox.Show("进入游戏", "进入游戏", MessageBoxType.Confirm);
        }
    }
}

配置表新加字段

对于一个配置表上新加的字段,需要在Common项目的Data文件夹下的对应文件(例如CharacterDefine.cs)中添加上,重新生成解决方案,然后把生成的Lib->common->bin->Debug文件夹下的Common.dll,Common.pdb,protobuf-net.dll,Protocol.dll,Protocol.pdb拷贝到客户端的Assets->References文件夹。
注意:Common是一个独立的项目,客户端和服务端共同读取,作为结构定义。

创建角色前后端通信

客户端

UICharacterSelect新增代码:

// 发送了请求
public void OnClickCreate()
{
    if (string.IsNullOrEmpty(this.charName.text))
    {
        MessageBox.Show("请输入角色名称");
        return;
    }
    UserService.Instance.SendCharacterCreate(this.charName.text, this.charClass);

}
// 服务器返回后的响应
void OnCharacterCreate(Result result, string message)
{
    if (result == Result.Success)
    {
        InitCharacterSelect(true);

    }
    else
        MessageBox.Show(message, "错误", MessageBoxType.Error);
}
public void InitCharacterSelect(bool init)
{
    panelCreate.SetActive(false);
    panelSelect.SetActive(true);

    if (init)
    {
        foreach (var old in uiChars)
        {
            Destroy(old);
        }
        uiChars.Clear();


    }
}

UserService新增代码:

// 新增注册
public UnityEngine.Events.UnityAction<Result, string> OnCharacterCreate;
public UserService(){
    MessageDistributer.Instance.Subscribe<UserCreateCharacterResponse>(this.OnUserCreateCharacter);
}
public void Dispose()
{
    MessageDistributer.Instance.Unsubscribe<UserCreateCharacterResponse>(this.OnUserCreateCharacter);
}
public void SendCharacterCreate(string charName, CharacterClass cls)
{
    Debug.LogFormat("SendCharacterCreate::charName :{0} class:{1}", charName, cls);
    NetMessage message = new NetMessage();
    message.Request = new NetMessageRequest();
    message.Request.createChar = new UserCreateCharacterRequest();
    message.Request.createChar.Name = charName;
    message.Request.createChar.Class = cls;
    // 如果没有连上则重连,连上了则通过NetClient类发送消息
    if (this.connected && NetClient.Instance.Connected)
    {
        this.pendingMessage = null;           // 相当于队列,链接后自动发送消息
        NetClient.Instance.SendMessage(message);
    }
    else
    {
        this.pendingMessage = message;
        this.ConnectToServer();
    }
}
void OnUserCreateCharacter(object sender, UserCreateCharacterResponse response)
{
    Debug.LogFormat("OnUserCreateCharacter:{0} [{1}]", response.Result, response.Errormsg);

    if (response.Result == Result.Success)
    {
        Models.User.Instance.Info.Player.Characters.Clear();
        Models.User.Instance.Info.Player.Characters.AddRange(response.Characters);
    }
    if(this.OnCharacterCreate != null)
    {
        this.OnCharacterCreate(response.Result, response.Errormsg);
    }
}
服务端:

UserService新增代码:

public UserService()
{
    MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<UserCreateCharacterRequest>(this.OnCreateCharacter);
}
void OnCreateCharacter(NetConnection<NetSession> sender, UserCreateCharacterRequest request)
{
    Log.InfoFormat("OnCreateCharacter: Name:{0}  Class:{1}", request.Name, request.Class);
    TCharacter character = new TCharacter()
    {
        Name = request.Name,
        Class = (int)request.Class,
        TID = (int)request.Class,
        MapID = 1,
        MapPosX = 5000,
        MapPosY = 4000,
        MapPosZ = 820,
    };
    // 把角色信息添加到数据库,为了防止数据变化,需要返回一下
    character = DBService.Instance.Entities.Characters.Add(character);
    // 更新内存中的角色信息
    sender.Session.User.Player.Characters.Add(character);
    DBService.Instance.Entities.SaveChanges();

    NetMessage message = new NetMessage();
    message.Response = new NetMessageResponse();
    message.Response.createChar = new UserCreateCharacterResponse();
    message.Response.createChar.Result = Result.Success;
    message.Response.createChar.Errormsg = "None";
    // 传回列表数据
    foreach (var c in sender.Session.User.Player.Characters)
    {
        NCharacterInfo info = new NCharacterInfo();
        info.Id = c.ID;
        info.Name = c.Name;
        info.Class = (CharacterClass)c.Class;
        message.Response.createChar.Characters.Add(info);
    }
        
    byte[] data = PackageHandler.PackMessage(message);
    sender.SendData(data, 0, data.Length);
}

选择角色

UICharacterSelect新增代码

public void InitCharacterSelect(bool init)
{
    panelCreate.SetActive(false);
    panelSelect.SetActive(true);

    if (init)
    {
        // 清除列表
        foreach (var old in uiChars)
        {
            Destroy(old);
        }
        uiChars.Clear();
        // 读取角色列表
        for(int i = 0; i < User.Instance.Info.Player.Characters.Count; i++)
        {
            // 创建对应的内容块
            GameObject go = Instantiate(uiCharInfo, this.uiCharList);
            UICharInfo chrInfo = go.GetComponent<UICharInfo>();
            // 获取角色信息
            chrInfo.info = User.Instance.Info.Player.Characters[i];
            Button button = go.GetComponent<Button>();
            int idx = i;
            // 监听按钮,按下的时候,切换名字
            button.onClick.AddListener(() => { OnSelectCharacter(idx); });
            uiChars.Add(go); 
            go.SetActive(true);

        }

    }
}
public void OnSelectCharacter(int idx)
{
    this.selectCharacterIdx = idx;
    var cha = User.Instance.Info.Player.Characters[idx];
    Debug.LogFormat("Select Char:[{0}]{1}[{2}]", cha.Id, cha.Name, cha.Class);
    User.Instance.CurrentCharacter = cha;
    characterView.CurrectCharacter = idx;
    // 优化UI
    for(int i = 0;i < User.Instance.Info.Player.Characters.Count; i++)
    {
        UICharInfo ci = this.uiChars[i].GetComponent<UICharInfo>();
        ci.Selected = idx == i;
    }
}

进入主城

对象关系:

实体关系:

角色管理器

用来管理角色生成,删除,查询等
客户端:

using SkillBridge.Message;
using Models;
using Common.Data;
using Entities;
using UnityEngine.Events;

namespace Services
{
    class CharacterManager : Singleton<CharacterManager>, IDisposable
    {
        public Dictionary<int, Character> Characters = new Dictionary<int, Character>();
        public UnityAction<Character> OnCharaterEnter;

        public CharacterManager()
        {


        }
        public int CurrentMapId { get; private set; }

        public void Dispose()
        {
        }
        public void init()
        {

        }
        public void Clear()
        {
            this.Characters.Clear();
        }
        public void AddCharacter(NCharacterInfo cha)
        {
            Debug.LogFormat("AddCharacter:{0}:{1} Map:{2},Entity:{3}", cha.Id, cha.Name, cha.mapId, cha.Entity.String());
            Character character = new Character(cha);
            // 把角色存入字典中
            this.Characters[cha.Id] = character;
            if(OnCharaterEnter != null)
            {
                OnCharaterEnter(character);
            }
        }
         
        public void RemoveCharacter(int characterId)
        {
            Debug.LogFormat("RemoveCharacter:{0}", characterId);
            this.Characters.Remove(characterId);
        }

    }
}

服务端:

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

namespace GameServer.Managers
{
    class CharacterManager : Singleton<CharacterManager>
    {
        public Dictionary<int, Character> Characters = new Dictionary<int, Character>();

        public CharacterManager()
        {
        }

        public void Dispose()
        {
        }

        public void Init()
        {

        }

        public void Clear()
        {
            this.Characters.Clear();
        }

        public Character AddCharacter(TCharacter cha)
        {
            Character character = new Character(CharacterType.Player, cha);
            this.Characters[cha.ID] = character;
            return character;
        }


        public void RemoveCharacter(int characterId)
        {
            this.Characters.Remove(characterId);
        }
    }
}

角色

客户端:

using UnityEngine;
using SkillBridge.Message;

namespace Entities
{
    public class Entity
    {
        public int entityId;

        // 客户端参与运输的值
        public Vector3Int position;
        public Vector3Int direction;
        public int speed;

        // 网络发过来的纯数据,每一次更新都会同时给实体数据更新
        private NEntity entityData;
        public NEntity EntityData
        {
            get {
                return entityData;
            }
            set {
                entityData = value;
                this.SetEntityData(value);
            }
        }
        // 数据更新
        public Entity(NEntity entity)
        {
            this.entityId = entity.Id;
            this.entityData = entity;
            this.SetEntityData(entity);
        }
        // 实时更新位置,并把实体数据转换为网络数据
        public virtual void OnUpdate(float delta)
        {
            if (this.speed != 0)
            {
                Vector3 dir = this.direction;
                this.position += Vector3Int.RoundToInt(dir * speed * delta / 100f);
            }
            entityData.Position.FromVector3Int(this.position);
            entityData.Direction.FromVector3Int(this.direction);
            entityData.Speed = this.speed;
        }
        // 把网络数据转换为实体数据
        public void SetEntityData(NEntity entity)
        {
            this.position = this.position.FromNVector3(entity.Position);
            this.direction = this.direction.FromNVector3(entity.Direction);
            this.speed = entity.Speed;
        }
    }
}
using SkillBridge.Message;
using UnityEngine;

namespace Entities
{
    public class Character : Entity
    {
        // 角色的网络信息和配置表信息
        public NCharacterInfo Info;
        public Common.Data.CharacterDefine Define;

        public string Name
        {
            get
            {
                if (this.Info.Type == CharacterType.Player)
                    return this.Info.Name;
                else
                    return this.Define.Name;
            }
        }

        public bool IsPlayer
        {
            get { return this.Info.Id == Models.User.Instance.CurrentCharacter.Id; }
        }

        public Character(NCharacterInfo info) : base(info.Entity)
        {
            this.Info = info;
            this.Define = DataManager.Instance.Characters[info.Tid];
        }
        // 下面是几种移动方法
        public void MoveForward()
        {
            Debug.LogFormat("MoveForward");
            this.speed = this.Define.Speed;
        }

        public void MoveBack()
        {
            Debug.LogFormat("MoveBack");
            this.speed = -this.Define.Speed;
        }

        public void Stop()
        {
            Debug.LogFormat("Stop");
            this.speed = 0;
        }

        public void SetDirection(Vector3Int direction)
        {
            Debug.LogFormat("SetDirection:{0}", direction);
            this.direction = direction;
        }

        public void SetPosition(Vector3Int position)
        {
            Debug.LogFormat("SetPosition:{0}", position);
            this.position = position;
        }
    }
}

服务端:
Character:

namespace GameServer.Entities
{
    class Character : CharacterBase
    {
        // 这里包含了db的数据
        public TCharacter Data;
        public Character(CharacterType type,TCharacter cha):
            base(new Core.Vector3Int(cha.MapPosX, cha.MapPosY, cha.MapPosZ),new Core.Vector3Int(100,0,0))
        {
            this.Data = cha;
            // 这里又包含了网络的数据
            this.Info = new NCharacterInfo();
            this.Info.Type = type;
            this.Info.Id = cha.ID;
            this.Info.Name = cha.Name;
            this.Info.Level = 1;//cha.Level;
            this.Info.Tid = cha.TID;
            this.Info.Class = (CharacterClass)cha.Class;
            this.Info.mapId = cha.MapID;
            this.Info.Entity = this.EntityData;
            //this.Define = DataManager.Instance.Characters[this.Info.Tid];
        }
    }
}

CharacterBase:


namespace GameServer.Entities
{
    class CharacterBase : Entity
    {

        public int Id
        {
            get
            {
                return this.entityId;
            }
        }
        public NCharacterInfo Info;
        public CharacterDefine Define;

        public CharacterBase(Vector3Int pos, Vector3Int dir):base(pos,dir)
        {

        }

        public CharacterBase(CharacterType type, int tid, int level, Vector3Int pos, Vector3Int dir) :
           base(pos, dir)
        {
            this.Info = new NCharacterInfo();
            this.Info.Type = type;
            this.Info.Level = level;
            this.Info.Tid = tid;
            this.Info.Entity = this.EntityData;
            //this.Define = DataManager.Instance.Characters[this.Info.Tid];
            this.Info.Name = this.Define.Name;
        }
    }
}

CharacterBase又继承于Entity,Entity记录基本的位置方向速度等信息。

地图管理器

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common;
using GameServer.Models;

namespace GameServer.Managers
{
    class MapManager : Singleton<MapManager>
    {
        Dictionary<int, Map> Maps = new Dictionary<int, Map>();

        public void Init()
        {
            // 导入配置表中的地图
            foreach (var mapdefine in DataManager.Instance.Maps.Values)
            {
                Map map = new Map(mapdefine);
                Log.InfoFormat("MapManager.Init > Map:{0}:{1}", map.Define.ID, map.Define.Name);
                this.Maps[mapdefine.ID] = map;
            }
        }
        // 相当于重载操作符,调用时可以写 MapManager.Instance[dbchar.MapID].CharacterEnter(sender, character);
        public Map this[int key]
        {
            get
            {
                return this.Maps[key];
            }
        }
        // 带有自主服务的需要Update
        public void Update()
        {
            foreach(var map in this.Maps.Values)
            {
                map.Update();
            }
        }
    }
}

注意:服务器端也有一个DataManger用来加载配置表,加载位置位于GameServer->bin->Debug->Data下,没有这个文件夹要创一个把txt文件放进去

服务端代码

UserService新增:

public UserService()
{
    MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<UserGameEnterRequest>(this.OnGameEnter);
}
void OnGameEnter(NetConnection<NetSession> sender, UserGameEnterRequest request)
{
    // 由db的角色生成实体角色
    TCharacter dbchar = sender.Session.User.Player.Characters.ElementAt(request.characterIdx);
    Log.InfoFormat("UserGameEnterRequest: CharacterID:{0}:{1}  Map:{2}", dbchar.Name, dbchar.MapID);
    Character character = CharacterManager.Instance.AddCharacter(dbchar);

    NetMessage message = new NetMessage();
    message.Response = new NetMessageResponse();
    message.Response.gameEnter = new UserGameEnterResponse();
    message.Response.gameEnter.Result = Result.Success;
    message.Response.gameEnter.Errormsg = "None";
    byte[] data = PackageHandler.PackMessage(message);
    sender.SendData(data, 0, data.Length);

    // 更新内存角色,并让角色进入地图(重要!!!)
    sender.Session.Character = character;
    MapManager.Instance[dbchar.MapID].CharacterEnter(sender, character);
}

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;

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>();


        internal Map(MapDefine define)
        {
            this.Define = define;
        }

        internal void Update()
        {
        }

        /// 角色进入地图,这个方法和UserService作用一样,从其中分离出来了一部分
        internal void CharacterEnter(NetConnection<NetSession> conn, Character character)
        {
            Log.InfoFormat("CharacterEnter: Map:{0} characterId:{1}", this.Define.ID, character.Id);
            // 这个Info是网络信息NCharacterInfo 
            character.Info.mapId = this.ID;

            NetMessage message = new NetMessage();
            message.Response = new NetMessageResponse();
            message.Response.mapCharacterEnter = new MapCharacterEnterResponse();
            message.Response.mapCharacterEnter.mapId = this.Define.ID;
            message.Response.mapCharacterEnter.Characters.Add(character.Info);
            // 把角色进入地图的信息发送给其他的人,并添加所有的角色到信息中
            foreach (var kv in this.MapCharacters)
            {
                message.Response.mapCharacterEnter.Characters.Add(kv.Value.character.Info);
                this.SendCharacterEnterMap(kv.Value.connection, character.Info);
            }
            // 地图把自己存进去
            this.MapCharacters[character.Id] = new MapCharacter(conn, character);
            // 发送信息
            byte[] data = PackageHandler.PackMessage(message);
            conn.SendData(data, 0, data.Length);
        }
        // 广播给其他人
        void SendCharacterEnterMap(NetConnection<NetSession> conn, NCharacterInfo character)
        {
            NetMessage message = new NetMessage();
            message.Response = new NetMessageResponse();

            message.Response.mapCharacterEnter = new MapCharacterEnterResponse();
            message.Response.mapCharacterEnter.mapId = this.Define.ID;
            message.Response.mapCharacterEnter.Characters.Add(character);

            byte[] data = PackageHandler.PackMessage(message);
            conn.SendData(data, 0, data.Length);
        }
    }
}

客户端代码

UICharacterSelect:

 // 进入游戏
public void OnClickPlay()
{
    if (selectCharacterIdx >= 0)
    {
        UserService.Instance.SendGameEnter(selectCharacterIdx);
    }
}

UserService新增代码:

public UserService()
{

    MessageDistributer.Instance.Subscribe<UserGameEnterResponse>(this.OnGameEnter);
    MessageDistributer.Instance.Subscribe<MapCharacterEnterResponse>(this.OnCharacterEnter);
}

public void SendGameEnter(int characterIdx)
{
    Debug.LogFormat("SendGameEnter::characterIdx :{0} ", characterIdx);
    NetMessage message = new NetMessage();
    message.Request = new NetMessageRequest();
    message.Request.gameEnter = new UserGameEnterRequest();
    message.Request.gameEnter.characterIdx = characterIdx;
        
    NetClient.Instance.SendMessage(message);

}

void OnGameEnter(object sender, UserGameEnterResponse response)
{

}

void OnCharacterEnter(object sender, MapCharacterEnterResponse message)
{
    Debug.LogFormat("OnCharacterEnter::MapID :{0} ", message.mapId);
    // 获取用户的地图信息,加载场景
    NCharacterInfo info = message.Characters[0];
    User.Instance.CurrentCharacter = info;
    SceneManager.Instance.LoadScene(DataManager.Instance.Maps[message.mapId].Resource);
}

这个时候我们需要创建一个MapService文件,用来存放地图相关的服务。
MapService:

using System;
using Network;
using UnityEngine;
using SkillBridge.Message;
using Models;
using Common.Data;

namespace Services
{
    class MapService : Singleton<MapService>, IDisposable
    {
        public MapService()
        {
            MessageDistributer.Instance.Subscribe<MapCharacterEnterResponse>(this.OnMapCharacterEnter);
            MessageDistributer.Instance.Subscribe<MapCharacterLeaveResponse>(this.OnMapCharacterLeave);

        }

        public int CurrentMapId { get; private set; }

        public void Dispose()
        {
            MessageDistributer.Instance.Unsubscribe<MapCharacterEnterResponse>(this.OnMapCharacterEnter);
            MessageDistributer.Instance.Unsubscribe<MapCharacterLeaveResponse>(this.OnMapCharacterLeave);
        }
        public void init()
        {

        }
        // 处理服务器发来进入地图请求的响应
        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.Id == cha.Id)
                {
                    User.Instance.CurrentCharacter = cha;
                }
                CharacterManager.Instance.AddCharacter(cha);
            }
            // 确认地图Id,正式进入地图
            if(CurrentMapId != response.mapId)
            {
                this.EnterMap(response.mapId);
                this.CurrentMapId = response.mapId;
            }
        }
        private void EnterMap(int mapId)
        {
            if (DataManager.Instance.Maps.ContainsKey(mapId))
            {
                MapDefine map = DataManager.Instance.Maps[mapId];
                SceneManager.Instance.LoadScene(map.Resource);
            }
            else
                Debug.LogErrorFormat("EnterMap:Map {0} not existed", mapId);
        }
    }
}

玩家输入

PlayerInputController:

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

using Entities;
using SkillBridge.Message;

public class PlayerInputController : MonoBehaviour {

    public Rigidbody rb;
    SkillBridge.Message.CharacterState state;

    public Character character;

    public float rotateSpeed = 2.0f;

    public float turnAngle = 10;

    public int speed;

    public EntityController entityController;

    public bool onAir = false;

    // Use this for initialization
    void Start () {
        // 默认状态
        state = SkillBridge.Message.CharacterState.Idle;
        if(this.character == null)
        {
            // 加载配置
            DataManager.Instance.Load();
            // 初始化创建用户信息(协议)
            NCharacterInfo cinfo = new NCharacterInfo();
            cinfo.Id = 1;
            cinfo.Name = "Test";
            cinfo.Tid = 1;
            cinfo.Entity = new NEntity();
            cinfo.Entity.Position = new NVector3();
            cinfo.Entity.Direction = new NVector3();
            cinfo.Entity.Direction.X = 0;
            cinfo.Entity.Direction.Y = 100;
            cinfo.Entity.Direction.Z = 0;
            // 协议变实体
            this.character = new Character(cinfo);

            if (entityController != null) entityController.entity = this.character;
        }
    }


    void FixedUpdate()
    {
        // 检查有无角色挂载
        if (character == null)
            return;

        // 获取键盘输入,误差校验
        float v = Input.GetAxis("Vertical");
        if (v > 0.01)
        {
            if (state != SkillBridge.Message.CharacterState.Move)
            {
                // 改状态,改值,改动画
                state = SkillBridge.Message.CharacterState.Move;
                this.character.MoveForward();
                this.SendEntityEvent(EntityEvent.MoveFwd);
            }
            // 执行移动,LogicToWorld将向量除以100,9.81基础值
            this.rb.velocity = this.rb.velocity.y * Vector3.up + GameObjectTool.LogicToWorld(character.direction) * (this.character.speed + 9.81f) / 100f;
        }
        else if (v < -0.01)
        {
            if (state != SkillBridge.Message.CharacterState.Move)
            {
                state = SkillBridge.Message.CharacterState.Move;
                this.character.MoveBack();
                this.SendEntityEvent(EntityEvent.MoveBack);
            }
            this.rb.velocity = this.rb.velocity.y * Vector3.up + GameObjectTool.LogicToWorld(character.direction) * (this.character.speed + 9.81f) / 100f;
        }
        else
        {
            if (state != SkillBridge.Message.CharacterState.Idle)
            {
                state = SkillBridge.Message.CharacterState.Idle;
                this.rb.velocity = Vector3.zero;
                this.character.Stop();
                this.SendEntityEvent(EntityEvent.Idle);
            }
        }

        if (Input.GetButtonDown("Jump"))
        {
            this.SendEntityEvent(EntityEvent.Jump);
        }
        // 处理旋转
        float h = Input.GetAxis("Horizontal");
        if (h<-0.1 || h>0.1)
        {
            this.transform.Rotate(0, h * rotateSpeed, 0);
            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));
                rb.transform.forward = this.transform.forward;
                this.SendEntityEvent(EntityEvent.None);
            }

        }
        //Debug.LogFormat("velocity {0}", this.rb.velocity.magnitude);
    }
    Vector3 lastPos;
    float lastSync = 0;
    private void LateUpdate()
    {
        // 位置改变作平滑移动
        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;
    }
    // 用来改变动画状态
    void SendEntityEvent(EntityEvent entityEvent)
    {
        if (entityController != null)
            entityController.OnEntityEvent(entityEvent);
    }
}

但对于其他玩家控制的角色我们主要控制的的是数据驱动的EntityController:

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


public class EntityController : MonoBehaviour
{

    public Animator anim;
    public Rigidbody rb;
    private AnimatorStateInfo currentBaseState;

    public Entity entity;

    public UnityEngine.Vector3 position;
    public UnityEngine.Vector3 direction;
    Quaternion rotation;

    public UnityEngine.Vector3 lastPosition;
    Quaternion lastRotation;

    public float speed;
    public float animSpeed = 1.5f;
    public float jumpPower = 3.0f;

    public bool isPlayer = false;

    // Use this for initialization
    void Start () {
        if (entity != null)
        {
            this.UpdateTransform();
        }
        // 不是玩家的话关闭重力的影响
        if (!this.isPlayer)
            rb.useGravity = false;
    }
    // 根据entity中的数据更新该实体
    void UpdateTransform()
    {
        this.position = GameObjectTool.LogicToWorld(entity.position);
        this.direction = GameObjectTool.LogicToWorld(entity.direction);

        this.rb.MovePosition(this.position);
        this.transform.forward = this.direction;
        this.lastPosition = this.position;
        this.lastRotation = this.rotation;
    }
	
    void OnDestroy()
    {
        if (entity != null)
            Debug.LogFormat("{0} OnDestroy :ID:{1} POS:{2} DIR:{3} SPD:{4} ", this.name, entity.entityId, entity.position, entity.direction, entity.speed);

        if(UIWorldElementManager.Instance!=null)
        {
            UIWorldElementManager.Instance.RemoveCharacterNameBar(this.transform);
        }
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        if (this.entity == null)
            return;
        // 执行移动操作
        this.entity.OnUpdate(Time.fixedDeltaTime);

        if (!this.isPlayer)
        {
            this.UpdateTransform();
        }
    }
    // 动画控制
    public void OnEntityEvent(EntityEvent entityEvent)
    {
        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;
        }
    }
}

主城地图管理

这个类用于处理角色进入地图的逻辑:
GameObjectManager:

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

using Entities;
using Services;
using SkillBridge.Message;
// 单例切换地图不销毁
public class GameObjectManager : MonoSingleton<GameObjectManager>
{
    // 管理ID和角色
    Dictionary<int, GameObject> Characters = new Dictionary<int, GameObject>();
    // 不要直接用Start否则会销毁这个管理器
    protected override void OnStart()
    {
        // 在进入之前,对其他已经进入的角色创建对象
        StartCoroutine(InitGameObjects());
        CharacterManager.Instance.OnCharacterEnter = OnCharacterEnter;    // 注册了一个角色进入事件
    }

    private void OnDestroy()
    {
        CharacterManager.Instance.OnCharacterEnter = null;
    }

    void Update()
    {

    }
    // 处理角色进入逻辑
    void OnCharacterEnter(Character cha)
    {
        CreateCharacterObject(cha);
    }
    
    IEnumerator InitGameObjects()
    {
        foreach (var cha in CharacterManager.Instance.Characters.Values)
        {
            CreateCharacterObject(cha);
            yield return null;
        }
    }

    private void CreateCharacterObject(Character character)
    {
        if (!Characters.ContainsKey(character.entityId) || Characters[character.entityId] == null)
        {
            // 加载配置表中的资源
            Object obj = Resloader.Load<Object>(character.Define.Resource);
            if(obj == null)
            {
                Debug.LogErrorFormat("Character[{0}] Resource[{1}] not existed.",character.Define.TID, character.Define.Resource);
                return;
            }
            // 创建角色,起名字,角色创建在这个管理器的子节点下
            GameObject go = (GameObject)Instantiate(obj,this.transform);
            go.name = "Character_" + character.Info.Id + "_" + character.Info.Name;
            // 存入字典中
            Characters[character.Info.Id] = go;
            // 添加UI管理
            UIWorldElementManager.Instance.AddCharacterNameBar(go.transform, character);
        }
        // 这里为了防止角色还没完全退出的情况,复用游戏对象,传输信息即可
        this.InitGameObject(Characters[character.entityId], character);
    }

    private void InitGameObject(GameObject go,Character character)
    {
        go.transform.position = GameObjectTool.LogicToWorld(character.position);
        go.transform.forward = GameObjectTool.LogicToWorld(character.direction);
        EntityController ec = go.GetComponent<EntityController>();
        if (ec != null)
        {
            ec.entity = character;
            ec.isPlayer = character.IsPlayer;
        }

        PlayerInputController pc = go.GetComponent<PlayerInputController>();
        if (pc != null)
        {
            // 当这个角色是玩家控制才启用这个脚本,并挂载摄像机
            if (character.Info.Id == Models.User.Instance.CurrentCharacter.Id)
            {
                User.Instance.CurrentCharacterObject = go;
                MainPlayerCamera.Instance.player = go;
                pc.enabled = true;
                pc.character = character;
                pc.entityController = ec;
            }
            else
            {
                pc.enabled = false;
            }
        }
        UIWorldElementManager.Instance.AddCharacterNameBar(go.transform, character);  //添加角色头顶的姓名条
    }
}

逻辑图

UI元素

管理头顶名称等信息:

using Entities;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIWorldElementManager : MonoSingleton<UIWorldElementManager>
{
    public GameObject nameBarPrefab;
    private Dictionary<Transform, GameObject> elements = new Dictionary<Transform, GameObject>();

    public void AddCharacterNameBar(Transform owner,Character character)
    {
        GameObject goNameBar = Instantiate(nameBarPrefab, this.transform);
        goNameBar.name = "NameBar" + character.entityId;
        goNameBar.GetComponent<UIWorldElement>().owner = owner;
        goNameBar.GetComponent<UINameBar>().character = character;
        goNameBar.SetActive(true);
        this.elements[owner] = goNameBar;
    }
    public void RemoveCharacterNameBar(Transform owner)
    {
        if (this.elements.ContainsKey(owner))
        {
            Destroy(this.elements[owner]);
            this.elements.Remove(owner);
        }
    }
}

小地图

知识点:

  1. 小地图的资源制作方式
  2. 小地图的UI元素
  3. 小地图的配置和加载
  4. 小地图与场景的坐标映射
  5. 地图资源的制作方式:实时渲染、预渲染顶视图+润色、纯美术制作

添加配置表字段

  1. 在配置表中添加对应的列
  2. 在Common工程Data文件夹对应的文件中加上字段,并重新生成解决方案
  3. 到Src->Lib->Common->bin->Debug文件夹下复制Common和Protobuf文件(如果修改了协议),复制到Client->Asset->References。

同步客户端服务端Dll文件

代码

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

public class UIMinimap : MonoBehaviour {

    public Collider minimapBoundingBox;
    public Image minimap;
    public Image arrow;
    public Text mapName;

    private Transform playerTransform;
	void Start () {
        this.InitMap();
    }

    void InitMap()
    {
        // 从User中读出数据
        this.mapName.text = User.Instance.CurrentMapData.Name;
        // 加载小地图
        if (this.minimap.overrideSprite == null)
            this.minimap.overrideSprite = MinimapManager.Instance.LoadCurrentMinimap();

        this.minimap.SetNativeSize();
        this.minimap.transform.localPosition = Vector3.zero;
        // 缓存一下,不要频繁访问单例
        this.playerTransform = User.Instance.CurrentCharacterObject.transform;
    }
	
	void Update () {
        float realWidth = minimapBoundingBox.bounds.size.x;
        float realHeight = minimapBoundingBox.bounds.size.z;

        float relaX = playerTransform.position.x - minimapBoundingBox.bounds.min.x;
        float relaY = playerTransform.position.z - minimapBoundingBox.bounds.min.z;
        // 确定地图中心点为玩家所在位置
        float pivotX = relaX / realWidth;
        float pivotY = relaY / realHeight;
        // 中心点
        this.minimap.rectTransform.pivot = new Vector2(pivotX, pivotY);
        this.minimap.rectTransform.localPosition = Vector2.zero;
        // 根据玩家旋转改变箭头朝向
        this.arrow.transform.eulerAngles = new Vector3(0, 0, -playerTransform.eulerAngles.y);
	}
}
using Models;

using UnityEngine;

namespace Managers
{
    class MinimapManager : Singleton<MinimapManager>
    {
        public Sprite LoadCurrentMinimap()
        {
            return Resloader.Load<Sprite>("UI/Minimap/" + User.Instance.CurrentMapData.MiniMap);
        }
    }
}

User新加字段:

namespace Models
{
    class User : Singleton<User>
    {
        public MapDefine CurrentMapData { get; set; }
        public GameObject CurrentCharacterObject { get; set; }
    }
}

在创建角色,和进入地图的时候赋值即可。

移动同步

同步指两个或两个以上随时间变化的量在变化过程中保一定的相对关系。游戏中需要同步的是角色信息,位置、状态。各个客户端的数据应当是同步的。
两种方式:状态同步、帧同步

我们MMO主要用状态同步。

离开游戏

移动同步

客户端:
PlayerInputController新增:

// 同步方法,在玩家按下操作或位置相差太远都会发生同步
void SendEntityEvent(EntityEvent entityEvent)
{
    // 用来改变动画状态
    if (entityController != null)
        entityController.OnEntityEvent(entityEvent);
    MapService.Instance.SendMapEntitySync(entityEvent, this.character.EntityData);
}

MapService新增:

public void SendMapEntitySync(EntityEvent entityEvent, NEntity entity)
{
    Debug.LogFormat("MapEntityUpdateRequest:ID :{0} POS:{1} DIR:{2} SPD: {3}", entity.Id, entity.Position.String(), entity.Direction.String(), entity.Speed);
    NetMessage message = new NetMessage();
    message.Request = new NetMessageRequest();
    message.Request.mapEntitySync = new MapEntitySyncRequest();
    message.Request.mapEntitySync.entitySync = new NEntitySync
    {
        Id = entity.Id,
        Event = entityEvent,
        Entity = entity
    };
    NetClient.Instance.SendMessage(message);
    
}

服务端:
MapService新增

public MapService()
    {

        MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<MapEntitySyncRequest>(this.OnMapEntitySync);

    }

private void OnMapEntitySync(NetConnection<NetSession> sender, MapEntitySyncRequest request)
    {
        Character character = sender.Session.Character;
        Log.InfoFormat("OnMapEntitySync: characterID: {0}:{1} Entity.Id:{2} Evt:{3} ENtity:{4}", character.Id, character.Info.Name, request.entitySync.Id, request.entitySync.Event, request.entitySync.Entity.String());

        MapManager.Instance[character.Info.mapId].UpdateEntity(request.entitySync);
    }
    internal void SendEntityUpdate(NetConnection<NetSession> connection,NEntitySync entity)
    {
        NetMessage message = new NetMessage();
        message.Response = new NetMessageResponse();

        message.Response.mapEntitySync = new MapEntitySyncResponse();
        message.Response.mapEntitySync.entitySyncs.Add(entity);

        byte[] data = PackageHandler.PackMessage(message);
        connection.SendData(data, 0, data.Length);
    }

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;
        }
        else
        {
            MapService.Instance.SendEntityUpdate(kv.Value.connection, entity);
        }
    }
}

回到客户端:
MapService新增:

void OnMapEntitySync(object sender, MapEntitySyncResponse response)
{
    System.Text.StringBuilder sb = new System.Text.StringBuilder();
    sb.AppendFormat("MapEntityUpdateResponse: Entitys:{0}", response.entitySyncs.Count);
    sb.AppendLine();
    
    foreach(var entity in response.entitySyncs)
    {
        EntityManager.Instance.OnEntitySync(entity); // 重要
        sb.AppendFormat("  [{0}]evt:{1} entity:{2}", entity.Id, entity.Event, entity.Entity.String());
        sb.AppendLine();
    }
    Debug.Log(sb.ToString());

}

EntityManager新增

internal void OnEntitySync(NEntitySync data)
{
    Entity entity = null;
    entities.TryGetValue(data.Id, out entity);    // ContainKey性能比这个消耗多一倍
    if(entity != null)
    {
        // 把收到的角色信息赋给本地实体
        if (data.Entity != null)
            entity.EntityData = data.Entity;
        // 发事件通知
        if (notifiers.ContainsKey(data.Id))
        {
            notifiers[entity.entityId].OnEntityChanged(entity);
            notifiers[entity.entityId].OnEntityEvent(data.Event);
        }
    }
}

打包项目

打包好之后注意把客户端目录下的Log4Net.xml复制到运行目录下,才有日志。并且创建一个Log文件夹,并且在提交之前在Player Setting,改.Net 4.0.

地图传送

代码

客户端

新增脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Common.Data;
using Services;

public class TeleporterObject : MonoBehaviour
{
    public int ID;
    Mesh mesh = null;
    void Start()
    {
        this.mesh = this.GetComponent<MeshFilter>().sharedMesh;
    }

    // 编辑器下可见,便于调试
#if UNITY_EDITOR
    void OnDrawGizmos()
    {
        // 绘制绿色框框
        Gizmos.color = Color.green;
        if(this.mesh != null){
            Gizmos.DrawWireMesh(this.mesh,this.transform.position + Vector3.up * this.transform.localScale.y * 0.5f,this.transform.rotation,this.transform.localScale);
        }
        // 绘制朝向小箭头
        UnityEditor.Handles.color = Color.red;
        UnityEditor.Handles.ArrowHandleCap(0, this.transform.position, this.transform.rotation, 1f, EventType.Repaint);
    }
#endif

    private void OnTriggerEnter(Collider other)
    {
        PlayerInputController playerInputController = other.GetComponent<PlayerInputController>();
        if(playerInputController != null && playerInputController.isActiveAndEnabled)
        {
            TeleporterDefine td = DataManager.Instance.Teleporters[this.ID];
            if(td == null)
            {
                Debug.LogErrorFormat("TeleporterObject:Character [{0}] Enter Teleporter [{1}],But TeleporterDefine not existed", playerInputController.character.Info.Name, this.ID);
                return;
            }
            Debug.LogFormat("TeleporterObject:Character [{0}] Enter Teleporter [{1}:{2}]", playerInputController.character.Info.Name, this.ID,td.Name);
            if(td.LinkTo > 0)
            {
                if (DataManager.Instance.Teleporters.ContainsKey(td.LinkTo))
                    MapService.Instance.SendMapTeleport(this.ID);
                else
                    Debug.LogErrorFormat("Teleporter [{0}] Enter Teleporter [{1}] error", td.ID, td.LinkTo);
            }
        }
    }
}

MapService新增

public void SendMapTeleport(int teleporterID)
{
    Debug.LogFormat("MapTeleportRequest:teleporterID:{0}", teleporterID);
    NetMessage message = new NetMessage();
    message.Request = new NetMessageRequest();
    message.Request.mapTeleport = new MapTeleportRequest();
    message.Request.mapTeleport.teleporterId = teleporterID;
    NetClient.Instance.SendMessage(message);

}

服务器

private void OnMapTeleport(NetConnection<NetSession> sender, MapTeleportRequest request)
{
    Character character = sender.Session.Character;
    Log.InfoFormat("OnMapTeleport: characterID: {0}:{1} TeleportedID{2}", character.Id, character.Data,request.teleporterId);
    // 判断是否包含该传送点
    if (!DataManager.Instance.Teleporters.ContainsKey(request.teleporterId))
    {
        Log.WarningFormat("Source TeleporterID [{0}] not existed", request.teleporterId);
        return;
    }
    // 判断是否包含传送点的连接点
    TeleporterDefine source = DataManager.Instance.Teleporters[request.teleporterId];
    if(source.LinkTo == 0 || !DataManager.Instance.Teleporters.ContainsKey(source.LinkTo))
    {
        Log.WarningFormat("Source TeleporterID [{0}] Linkto ID {1} not existed", request.teleporterId,source.LinkTo);
    }
    // 离开旧地图并进入新地图
    TeleporterDefine target = DataManager.Instance.Teleporters[source.LinkTo];
    MapManager.Instance[source.MapID].CharacterLeave(character);
    // 注意我们配置表没有填位置,但是却是有位置的,配置生成的是规则
    character.Position = target.Position;
    character.Direction = target.Direction;
    MapManager.Instance[target.MapID].CharacterEnter(sender, character);
}

扩展编辑器

我们需要在编辑器中扩展一个菜单项,让菜单可以生成传送点
首先,需要在Asset下建立一个Editor目录存放文件,创建一个MapTool文件如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
using UnityEditor;
using Common.Data;

public class MapTools : MonoBehaviour
{
    // 定义Unity菜单项,要用静态函数
    [MenuItem("Map Tools/Export Teleporters")]
    public static void ExportTeleporters()
    {
        DataManager.Instance.Load();
        Scene current = EditorSceneManager.GetActiveScene();
        string currentScene = current.name;
        if (current.isDirty)
        {
            EditorUtility.DisplayDialog("提示","请保存当前的场景","确定");
            return;
        }
        List<TeleporterObject> allTeleporters = new List<TeleporterObject>();
        foreach (var map in DataManager.Instance.Maps)
        {
            string sceneFile = "Assets/Levels/" + map.Value.Resource + ".unity";
            if (!System.IO.File.Exists(sceneFile))
            { 
                Debug.LogWarningFormat("Scene {0} not existed!", sceneFile);
                continue;
            }
            EditorSceneManager.OpenScene(sceneFile, OpenSceneMode.Single);

            TeleporterObject[] teleporters = GameObject.FindObjectsOfType<TeleporterObject>();
            foreach (var teleporter in teleporters)
            {
                if (!DataManager.Instance.Teleporters.ContainsKey(teleporter.ID))
                {
                    EditorUtility.DisplayDialog("错误", string.Format("地图:{0} 中配置的Teleporter{1}不存在", map.Value.Resource, teleporter.ID), "确定");
                    return;
                }                
                TeleporterDefine def = DataManager.Instance.Teleporters[teleporter.ID];
                // 检查地图ID
                if(def.MapID != map.Value.ID)
                {
                    EditorUtility.DisplayDialog("错误", string.Format("地图:{0} 中配置的Teleporter{1}不存在", map.Value.Resource, teleporter.ID), "确定");
                    return;
                }
                def.Position = GameObjectTool.WorldToLogicN(teleporter.transform.position);      // 这里导出了配置
                def.Direction = GameObjectTool.WorldToLogicN(teleporter.transform.forward);

            }
        }
        DataManager.Instance.SaveTeleporters();
        EditorSceneManager.OpenScene("Assets/Levels/" + currentScene + ".unity");
        EditorUtility.DisplayDialog("提示", "传送点导出完成", "确定");
    }
}

点击菜单项的该按钮后,客户端的配置文件会自动增加一项传送点的位置方向的数值。需要复制到服务器的配置文件中使用。


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