地图传送
创建传送点
建碰撞器触发
//位置归零
建一个传送门cube放到要传送的位置(这个teleporter1是传出的区域
这是从另一张地图传入时的传送门
创建一个脚本TeleporterObject给每个传送cube都绑上脚本
通过脚本,让传送门在编辑器下面还能绘制出来
给每个传送点编号
注意!这里的传送点cube要设置Layer:Teleport:因为角色(层级是Defalut)会触发传送点;而Default之间不能碰撞
把特效挂在传送点上
//把野外场景的传送也加上(并把传送门的id改了
碰撞检测
TelePorterObject:OnTriggerEnter
private void OnTriggerEnter(Collider other)
{
PlayerInputController playerInputController=other.GetComponent<PlayerInputController>();
//传入的对象是否有玩家控制器
if(playerInputController!=null&&playerInputController.isActiveAndEnabled)
{
//得到传送点的ID
TeleporterDefine teleDefine = DataManager.Instance.Teleporters[this.ID];
if(teleDefine==null)
{
//从角儿控制器取得角色character,第几个传送点
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 Telepoter [{1}:{2}] ",playerInputController.character.Info.Name, teleDefine.ID,teleDefine.Name); ;
if(teleDefine.LinkTo>0)
{
if(DataManager.Instance.Teleporters.ContainsKey(teleDefine.LinkTo))
MapService.Instance.SendMapTeleporter(this.ID);
else Debug.LogErrorFormat("Teleporter ID:{0} LinkID {1} error!",teleDefine.ID,teleDefine.LinkTo);
}
}
}
在MapService中发送进入传送点的信息SendMapTeleporter
SendMapTeleporter
public void SendMapTeleport(int teleporterID)
{
Debug.LogFormat("MapTeleporterRequest :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);
}
向客户端发送有角色进入传送点的信息
message MapTeleportRequest
{
int32 teleporterId = 1;
}
只需要传一个传送点id即可(也可以传地图的id,再传送点的id)
服务端的协议处理MapService:OnMapTeleport
在MapService()中,
订阅:
MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<MapTeleportRequest>(this.OnMapTeleport);
void OnMapTeleport(NetConnection<NetSession> sender,MapTeleportRequest request)
{
//得到客户端进行传送点传送的对象
Character character=sender.Session.Character;
Log.InfoFormat("OnMapTeleporter: characterID:{0}:{1} TeleporterId:{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 teleportDefine=DataManager.Instance.Teleporters[request.teleporterId];
if(teleportDefine.LinkTo==0||!DataManager.Instance.Teleporters.ContainsKey(teleportDefine.LinkTo))
{
Log.WarningFormat("Source TeleporterID [{0}] LinkTo ID [{1}] not existed", request.teleporterId, teleportDefine.LinkTo);
}
//从客户端传过来的传送点数据表teleportDefine.LinkTo:6
//取的key为6 传送目标点
TeleporterDefine teleporterDefine1 = DataManager.Instance.Teleporters[teleportDefine.LinkTo];
//角色所在的地图,角色离开处理
MapManager.Instance[teleportDefine.MapID].CharacterLeave(character);
//把新位置信息填充给角色
character.Position=teleporterDefine1.Position;
character.Direction=teleporterDefine1.Direction;
//角色进入新地图
MapManager.Instance[teleporterDefine1.MapID].CharacterEnter(sender,character);
}
//关于传送点配置表TeleporterDefine:
点击这里查看是否有TeleporterDefine配置表生成
扩展编辑器MapTool
在Asset/Editor目录下:
首先把DataManager(角色,传送门,地图之类的信息加载Load
获取当前场景
获取所有传送点
遍历所有的地图,得到地图文件.unity;打开每个场景
获取传送点,检查所有的传送点id在配置表中是否存在
传送点teleportDefine对应的地图id是否正确
把世界坐标转换成逻辑坐标存到配置表中
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
using UnityEditor;
using Common.Data;
public class MapTool : MonoBehaviour
{
[MenuItem("Map Tools/Export Teleportters")]
//扩展功能:static
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)
{
Debug.Log("传送点ID" + teleporter.ID);
if(!DataManager.Instance.Teleporters.ContainsKey(teleporter.ID))
{//检查传送点的id在配置表中是否存在
EditorUtility.DisplayDialog("错误", string.Format("地图:{0} 中配置的 Teleporter:[{1}]中不存在", map.Value.Resource, teleporter.ID), "确定");
return;
}
TeleporterDefine def=DataManager.Instance.Teleporters[teleporter.ID];
if (def.MapID != map.Value.ID)
{//地图配的mapID是否正确
EditorUtility.DisplayDialog("错误", string.Format("地图:{0} 中的配置的Teleporter:[{1}] MapID:{2} 错误", map.Value.Resource,teleporter.ID,def.MapID), "确定");
return;
}
def.Position=GameObjectTool.WorldToLogicN(teleporter.transform.position);
def.Direction=GameObjectTool.WorldToLogicN(teleporter.transform.forward);
}
}
//Save逻辑写在DataMangaer下,运行时是不会受影响的
DataManager.Instance.SaveTeleporters();
EditorSceneManager.OpenScene("Assets/Levels/" + currentScene + ".unity");
EditorUtility.DisplayDialog("提示", "传送点导出完成", "确定");
}
}
演示:
传送成功;
传送请求:1号传送点传送到野外的6号点
//从野外传回主城
5号传送点,传LinkTo2号传送点
//但是在野外的相机没有对着角色;在两个场景的切换时,角色会浮空
//Add:可以在场景切换时做一个Loading进度条掩盖
->Map01只有MainPlayerCamera带过来的相机发挥跟随角色的作用//创建角色时相机已经挂上了,删掉野外的一个Camera即可
关于到了新的场景中固定UI没有显示
把UIMainCity做成单例
(在加载新场景时UIMainCity会再创建实例
//可以看到现在加载到另一个场景,显示了UIMainCity和MainPlayerCamera以及UIWorldElementManager等;还有一些在每个场景中必要的GameObject:
//它们都是挂了单例脚本的物体
UI系统框架设计
UI的分类:
UI框架的设计:
补充:断开连接角色处理
关于在客户端与服务器断开连接,服务器不重启,重启客户端;DisConnected->Connected
登入主城发现客户端界面 有两个角色:因此每次断开连接时,要把数据session全部清理掉
在NetService:Disconnected方法中加上这://作用时清理数据
在NetSession中做修复Disconnected
删掉角色所有信息
internal void Disconnted()
{
if(this.Character!=null)
//角色离开
UserService.Instance.CharacterLeave(this.Character);
}
UserService:CharacterCreate
对于用户离开游戏OnGameLeave,里面有RemoveCharacter和map[mapid].CharacterLeave
我们重构这两句
并改成公有的://这样NetSession就可以引用了
演示
没有做断开连接角色处理的服务器页面:
没有角色离开
再进入主城是有上一次客户端数据的残留
进入主城后关掉客户端
已经做角色离开了:CharacterLeave
在启动客户端,进入主城
地图上只有一个角色
//关于刷新数据
例如小地图的mapImage
//小地图需要在世界场景下加一个BoundingBox;根据当前角色的位置更新在小地图上的位置
需要将每次切换场景时把角色数据都拉一次进来
在UIMinmap.cs中,只有在启动时才加载了小地图
UIMain
UIMainCity更名为UIMain//对应脚本也改掉
把UIMain做成了单例,这样每个场景都能有固定UI(小地图,技能栏;初次出现是在MainCity场景中,后面可以在这个场景下的UIMain节点下做各种UI物体
把initmap改为updatemap
UIMain.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Models;
using Services;
public class UIMain : MonoSingleton<UIMain>
{
public Text avatarName;
public Text avatarLevel;
protected override void OnStart()
{//在启动时候刷新
this.UpdateAvatar();
}
void UpdateAvatar()
{
//User是单例类,存放用户和角色的各种相关信息//CurrentCharacter存储网络传回来的信息(姓名角色等级..)
this.avatarName.text = string.Format("{0}[{1}]", User.Instance.CurrentCharacter.Name, User.Instance.CurrentCharacter.Id);
this.avatarLevel.text = User.Instance.CurrentCharacter.Level.ToString();
}
void Update()
{
}
public void BackToCharSelect()
{
SceneManager.Instance.LoadScene("CharSelect");
UserService.Instance.SendGameLeave();
}
}
切换地图,要换的小地图数据,在MinimapManager中管理这些数据
同时小地图管理器要知道小地图是哪个,这样就能对不同的小地图进行统一管理
现在的MinimapManager:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Analytics;
using Models;
namespace Managers
{
class MinimapManager : Singleton<MinimapManager>
{
public UIMinmap UIminimap;
private Collider minimapBoundingBox;
public Collider MinimapBoundingBox
{
get { return minimapBoundingBox; }
}
public Transform PlayerTransform
{
get
{
if (User.Instance.CurrentCharacterObject == null)
return null;
return User.Instance.CurrentCharacterObject.transform;
}
}
public Sprite LoadCurrentMinimap()
{
//返回图片所在的路径:图片资源放在了Resources下面
//这里用拼接字符串形成了完整路径
return Resloader.Load<Sprite>("UI/Minimap/" + User.Instance.CurrentMapData.MiniMap);
}
public void UpdateMinimap(Collider minimapBoundingBox)
{
//minimapBoundingBox change->告诉小地图:他变了
this.minimapBoundingBox=minimapBoundingBox;
if (this.UIminimap != null)
this.UIminimap.UpdateMap();
}
}
}
在UIMinmap中引入minimap对象
于是我们就能在MinimapManager管理器中做小地图的更新UpdateMinimap
public void UpdateMinimap(Collider minimapBoundingBox)
{
//minimapBoundingBox change->告诉小地图:他变了
this.minimapBoundingBox=minimapBoundingBox;
if (this.minimap != null)
this.minimap.UpdateMap();
}
在此方法中又调用UIMinimap中更新小地图的方法:UpdateMap
原来的方法中用的minmapBoundingBox在主城中通过public得到的,现在需要更新它
注意!每次切换地图是角色是重新创建的,角色的信息都会被删除,因此我们要把角色清空掉
如果不清空的话,Update里面的就不会更新了;
注意!现在小地图不要需要这一句:
切换场景(地图变化)时调用UpdateMinimap
MinimapManager:UpdateMinimap
public void UpdateMinimap(Collider minimapBoundingBox)
{
//minimapBoundingBox change->告诉小地图:他变了
this.minimapBoundingBox=minimapBoundingBox;
if (this.UIminimap != null)
this.UIminimap.UpdateMap();
}
如果每个地图有一个唯一的脚本,地图加载的时候脚本就执行
->在每个场景下创建一个MapRoot,再新建一个地图控制器MapController;
当前地图已经加载了就通知小地图管理器,更新小地图,并传入一个包围盒
MapController
public Collider minimapBoundingbox;
void Start()
{
MinimapManager.Instance.UpdateMinimap(minimapBoundingbox);
}
总结
地图控制器把包围传给-----小地图管理器的UpdateMinimap方法,传给-----小地图UpdateMap方法
MapController:
MinimapManager.Instance.UpdateMinimap(minimapBoundingbox);
MinimapManager:
this.UIminimap.UpdateMap();
UIminimap
this.minmapBoundingBox = MinimapManager.Instance.MinimapBoundingBox;
->UIMain是单例,UIMain下面有UIMinimap:
把每一个场景都做一个MapRoot绑上地图控制器拖上包围盒
启动演示:
//Add可以加一个加速按钮(背包里的滑板车//未做
UIManager
//是Singleton单例
各种弹出ui(如商店,NPC对话,任务栏)的共同事件汇总(框架)
UI元素(已经做好的prefab//被放在Resources/UI下面
展示面板:show
关闭面板:close
//里面做一些实例化或销毁
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class UIManager : MonoSingleton<UIManager>
{
class UIElement
{//UI元素
public string Resources;//资源路径
public bool Cache;
public GameObject Instance;
}
//用来保存定义的UI信息
private Dictionary<Type,UIElement>UIResources=new Dictionary<Type, UIElement>();
public UIManager()
{
this.UIResources.Add(typeof(UITest),new UIElement() { Resources="UI/UITest",Cache=true});
}
~UIManager()
{
}
public T Show<T>()
{
//声音播放
//SoundManager.Instance.PlaySound("ui_open");
Type type = typeof(T);
if(this.UIResources.ContainsKey(type))
{
UIElement UIElementinfo=this.UIResources[type];
if(UIElementinfo.Instance!=null)
{//如果这个UI元素有实例了,激活
UIElementinfo.Instance.SetActive(true);
}
else
{//从资源中加载prefab
UnityEngine.Object prefab=Resources.Load(UIElementinfo.Resources);
if(prefab==null)
{
return default(T);
}//实例化
UIElementinfo.Instance=(GameObject)GameObject.Instantiate(prefab);
}
return UIElementinfo.Instance.GetComponent<T>();
}
return default(T);
}
public void Close(Type type)
{
//SoundManager.Instance.PlaySound("ui_close");
if(this.UIResources.ContainsKey(type))
{
UIElement UIElementinfo=this.UIResources[type];
if(UIElementinfo.Cache)//如果启用了Cache则不销毁
UIElementinfo.Instance.SetActive(false);//?
else
{
GameObject.Destroy(UIElementinfo.Instance);
UIElementinfo.Instance = null;
}
}
}
}
UIWindows
委托接受UIWindows对象,和WindowsResult结果对象
委托类型 的OnClose事件
获取类型,结果类型
Close方法:有窗口才关闭
yes/no按钮的事件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class UIWindows : MonoBehaviour
{//给所有的UI当父类用
public delegate void CloseHandler(UIWindows sender, WindowResult result);
public event CloseHandler OnClose;
public virtual System.Type Type
{//获取类型
get
{
return this.GetType();
}
}
//内置了一个结果类型
public enum WindowResult
{
None=0,
Yes,
No,
}
public void Close(WindowResult result=WindowResult.None)
{
//做UIManager.Close;并且OnClose关闭窗口事件
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");
}
}
写一个关于UIManager为框架,UIWindows的子类:UITest
UITest
先把UI面板做好prefab放在Resources/UI
UITest脚本:
//继承UIWindows即可
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UITest : UIWindows
{
}
在UITest画布下要给按钮绑定事件,可以找到UIWindows的按钮事件
把做好的UITest画布放在
在UIManage中:
要先把UITest加到管理器中,管理器才能使用它:
this.UIResources.Add(typeof(UITest),new UIElement() { Resources="UI/UITest",Cache=true});
//后面若有UIShopCanvas;类似该语句添加
测试新加上来的UITest:
在UIMain中加一个按钮可以打开UITest
在UIMain中加一个测试事件
public void OnClickTest()
{
UIManager.Instance.Show<UITest>();
}
把事件绑到这些按钮上
演示:
在OnClickTest中执行一些UITest的方法
public void OnClickTest()
{
UITest uitest=UIManager.Instance.Show<UITest>();
//可以用uitest调用UItest的方法
uitest.SetTitle("新标题");
}
public class UITest : UIWindows
{
public Text Title;
public void SetTitle(string title)
{
this.Title.text = title;
}
}
//注意:UIManager继承的是普通单例,不是mono单例;不需要挂在场景中
Test_OnClose是UIWindows的方法,可以直接获取UITest的信息sender和UI窗口的点击情况
public void OnClickTest()
{
UITest uitest = UIManager.Instance.Show<UITest>();
//可以用uitest调用UItest的方法
uitest.SetTitle("新标题");
uitest.OnClose += Test_OnClose;
}
private void Test_OnClose(UIWindows sender,UIWindows.WindowResult result)
{//OnClose获取结果;即调用者负责获取调用的结果uitest.OnClose
//例如在改名后点击确认按钮,可以获取到改的名字是什么
//(sender as UITest).name
//在调用前或者后可以任意访问UI的各种值
MessageBox.Show("点击了对话框的:" + result, "对话框响应结果", MessageBoxType.Information);
}
点击确定和关闭按钮的MessageBox.Show: