目录
第5章: 主城UI逻辑及角色控制
制作主城场景
导入主城场景,并进行灯光烘焙
贴图文件
主城UI界面制作
跳转主城场景
创建MainCitySys和MainCityWnd两个脚本
//主城UI界面
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainCityWnd : WindowRoot
{
protected override void InitWnd()
{
base.InitWnd();
}
}
//主城业务系统
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainCitySys : SystemRoot
{
public static MainCitySys Instance = null;
public MainCityWnd mainCityWnd;
public override void InitSys()
{
base.InitSys();
Instance = this;
Debug.Log("Init MainCitySys...");
}
public void EnterMainCity()
{
resSvc.AsyncLoadScene(Constants.SceneMainCity,()=>
{
PECommon.Log("Enter MainCity...");
//TODO 加载游戏主角
//打开主城场景UI
mainCityWnd.SetWndState();
//播放主城里背景音乐
audioSvc.PlayBGMusic(Constants.BGMainCity);
//TODO 设置人物展示相机
});
}
}
在GameRoot里面加载主城系统
MainCitySys mainCitySys = GetComponent<MainCitySys>();
mainCitySys.InitSys();
在合适的地方调用主城显示的方法,主要是在LoginSys中
public void RspRename(GameMsg msg)
{
GameRoot.Instance.SetPlayerName(msg.rspRename.name);
//TODO
//跳转场景进入主城
//打开主城的界面
MainCitySys.Instance.EnterMainCity();
//关闭创建页面
createWnd.SetWndState(false);
}
public void RspLogin(GameMsg msg)
{
GameRoot.AddTips("登录成功");
GameRoot.Instance.SetPlayerData(msg.rspLogin);
if(msg.rspLogin.playerData.name == "")
{
//打开角色创建页面
createWnd.SetWndState();
}
else
{
//进入主城
MainCitySys.Instance.EnterMainCity();
}
//关闭登录页面
loginWnd.SetWndState(false);
}
运行程序,结果如下
增加角色属性
主城UI显示逻辑
在MainCityWnd中添加UI初始化显示的方法
public Text txtFight;
public Text txtPower;
public Image imgPowerPrg;
public Text txtLv;
public Text txtName;
public Text txtExpPrg;
private void RefreshUI()
{
PlayerData pd = GameRoot.Instance.PlayerData;
SetText(txtFight, PECommon.GetFightByProps(pd));
SetText(txtPower, "体力:" + pd.power+"/"+PECommon.GetPowerLimit(pd.lv));
imgPowerPrg.fillAmount = pd.power * 1.0f / PECommon.GetPowerLimit(pd.lv);
SetText(txtLv, pd.lv);
SetText(txtName, pd.name);
//expprg
}
上面的方法会用到一些PECommon中的公用方法
//计算战斗力
public static int GetFightByProps(PlayerData pd)
{
return pd.lv * 100 + pd.ad + pd.ap + pd.addef + pd.addef;
}
//计算体力限制
public static int GetPowerLimit(int lv)
{
return (lv - 1) / 10 * 150 + 150;
}
运行程序,结果如下
分段经验条制作
使用Grid Layout Group组件来进行分段经验条的制作
经验条自适应
首先要取到Transform组件
public Transform expPrgTrans;
然后在Constants中定义好标准的宽高
//屏幕标准宽高
public const int ScreenStandardWidth = 1334;
public const int ScreenStandardHeight = 750;
利用标准宽高和真实宽高的比例来自适应
GridLayoutGroup grid = expPrgTrans.GetComponent<GridLayoutGroup>();
float globalRate = 1.0f * Constants.ScreenStandardHeight / Screen.height;
float screenWidth = Screen.width * globalRate;
float width = (screenWidth - 182) / 10;
grid.cellSize = new Vector2(width, 7);
利用10个进度条来表示当前的经验值
int expPrgVal = (int)(pd.exp * 1.0f / PECommon.GetExpUpValByLv(pd.lv) * 100);
SetText(txtExpPrg, expPrgVal + "%");
int index = expPrgVal / 10;
for(int i =0;i<expPrgTrans.childCount;i++)
{
Image img = expPrgTrans.GetChild(i).GetComponent<Image>();
if (i < index)
img.fillAmount = 1;
else if(i == index)
{
img.fillAmount = expPrgVal % 10 * 1.0f / 10;
}
else
{
img.fillAmount = 0;
}
}
下线数据清理
为了清理Session避免错误判断账号在线,我们给每个session设置一个ID,然后对ServerSession进行修改。
using PENet;
using PEProtocol;
/// <summary>
/// 网络会话连接
/// </summary>
public class ServerSession:PESession<GameMsg>
{
//生成一个ID
public int sessionID = 0;
protected override void OnConnected()
{
sessionID = ServerRoot.Instance.GetSessionID();
PECommon.Log("SessionID" + sessionID + " Client Connect");
}
protected override void OnReciveMsg(GameMsg msg)
{
PECommon.Log("SessionID" + sessionID + " RcvPack CMD:" +((CMD)msg.cmd).ToString());
NetSvc.Instance.AddMsgQue(new MsgPack(this,msg));
}
protected override void OnDisConnected()
{
LoginSys.Instance.ClearOfflineData(this);
PECommon.Log("SessionID" + sessionID + " Client Offline");
}
}
其中ClearOfflineData()用来让队列中的session出队。
public void ClearOfflineData(ServerSession session)
{
cacheSvc.AcctOffLine(session);
}
public void AcctOffLine(ServerSession session)
{
foreach(var item in onLineAcctDic)
{
if(item.Value == session)
{
onLineAcctDic.Remove(item.Key);
break;
}
}
bool succ = onLineSessionDic.Remove(session);
PECommon.Log("Offline Result: SessionID" + session.sessionID + " " + succ);
}
主菜单动画制作
在MenuRoot上添加两个动画,分别控制主菜单的打开和关闭
动画播放控制
首先需要定义好使用的物体和组件
public Animation menuAni;
public Button btnMenu;
private bool menuState = true;//用来判断当前菜单的状态
然后在Constants中添加需要播放的音乐
public const string UIExtenBtn = "uiExtenBtn";
最后完成点击菜单按钮的函数,然后将其关联到指定按钮上
public void ClickMenuBtn()
{
audioSvc.PlayUIMusic(Constants.UIExtenBtn);
menuState = !menuState;
AnimationClip clip = null;
if (menuState)
clip = menuAni.GetClip("OpenMCMenu");
else
clip = menuAni.GetClip("CloseMCMenu");
menuAni.Play(clip.name);
}
摇杆插件制作1
首先创建三张图片来表示出摇杆
然后在MainCityWnd中进行定义
public Image imgTouch;
public Image imgDirBg;
public Image imgDirPoint;
在初始的时候,需要隐藏imgDirPoint,之后调用摇杆的方法
protected override void InitWnd()
{
base.InitWnd();
SetActive(imgDirPoint, false);
RegisterTouchEvts();
RefreshUI();
}
public void RegisterTouchEvts()
{
}
摇杆插件制作2
在Common文件夹中创建一个PEListener的脚本用来进行UI事件的监听
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
/// <summary>
/// UI事件监听插件
/// </summary>
public class PEListener : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
public Action<PointerEventData> onClickDown;
public Action<PointerEventData> onClickUp;
public Action<PointerEventData> onDrag;
public void OnPointerDown(PointerEventData eventData)//按下事件
{
if (onClickDown != null)
onClickDown(eventData);
}
public void OnPointerUp(PointerEventData eventData)//抬起事件
{
if (onClickUp != null)
onClickUp(eventData);
}
public void OnDrag(PointerEventData eventData)//拖拽事件
{
if (onDrag != null)
onDrag(eventData);
}
}
在MainCityWnd中给imgTouch添加上这个脚本,用来处理按下、抬起和拖拽事件,下面是处理按下事件
public void RegisterTouchEvts()
{
PEListener listener = imgTouch.gameObject.AddComponent<PEListener>();
listener.onClickDown = (PointerEventData evt) =>
{
imgDirBg.transform.position = evt.position;
};
}
摇杆插件制作3
在GameRoot里封装好这三个方法,方便之后的调用
//判断是否添加组件
protected T GetOrAddComponent<T>(GameObject go) where T:Component
{
T t = go.GetComponent<T>();
if (t == null)
t = go.AddComponent<T>();
return t;
}
protected void OnClickDown(GameObject go,Action<PointerEventData> cb)
{
PEListener listener = GetOrAddComponent<PEListener>(go);
listener.onClickDown = cb;
}
protected void OnClickUp(GameObject go, Action<PointerEventData> cb)
{
PEListener listener = GetOrAddComponent<PEListener>(go);
listener.onClickUp = cb;
}
protected void OnDrag(GameObject go, Action<PointerEventData> cb)
{
PEListener listener = GetOrAddComponent<PEListener>(go);
listener.onDrag = cb;
}
之前的按下案列封装后的写法
OnClickDown(imgTouch.gameObject, (PointerEventData evt) =>
{
imgDirBg.transform.position = evt.position;
});
摇杆插件制作4
这节完成松开事件和拖拽事件的方法,首先需要定义一些变量
private bool menuState = true;
private float pointDis;
private Vector2 startPos = Vector2.zero;
private Vector2 defaultPos = Vector2.zero;
//摇杆点标准距离
public const int ScreenOPDis = 90;
然后进行初始哈
//通过真实的高度和标准的高度对比算出摇杆的最大距离
pointDis = Screen.height * 1.0f / Constants.ScreenStandardHeight * Constants.ScreenOPDis;
defaultPos = imgDirBg.transform.position;
下面是三个函数的代码
public void RegisterTouchEvts()
{
PEListener listener = imgTouch.gameObject.AddComponent<PEListener>();
OnClickDown(imgTouch.gameObject, (PointerEventData evt) =>
{
startPos = evt.position;
SetActive(imgDirPoint);
imgDirBg.transform.position = evt.position;
});
OnClickUp(imgTouch.gameObject, (PointerEventData evt) =>
{
imgDirBg.transform.position = defaultPos;
SetActive(imgDirPoint, false);
imgDirPoint.transform.localPosition = Vector2.zero;
//TODO方向信息传递
Debug.Log(Vector2.zero);
});
OnDrag(imgTouch.gameObject, (PointerEventData evt) =>
{
Vector2 dir = evt.position - startPos;
float len = dir.magnitude;
if (len > pointDis)
{
//将长度限制到范围以内
Vector2 clampDir = Vector2.ClampMagnitude(dir, pointDis);
imgDirPoint.transform.position = startPos + clampDir;
}
else
imgDirPoint.transform.position = evt.position;
//TODO方向信息传递
Debug.Log(dir.normalized);
});
}
主角人物制作
主角运动控制
在主角身上添加一个PlayerController脚本
/****************************************************
文件:PlayerController.cs
作者:Sou
邮箱: 947662512@qq.com
日期:2019/7/26 19:54:28
功能:角色控制器
*****************************************************/
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Transform camTrans;
private Vector3 camOffset;
public Animator ani;
public CharacterController ctrl;
private Vector2 dir = Vector2.zero;
public Vector2 Dir
{
get
{
return dir;
}
set
{
if (value == Vector2.zero)
isMove = false;
else
isMove = true;
dir = value;
}
}
private bool isMove = false;
private void Start()
{
camTrans = Camera.main.transform;
camOffset = transform.position - camTrans.position;
}
private void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
Vector2 _dir = new Vector2(h, v).normalized;
if (_dir != Vector2.zero)
Dir = _dir;
else
Dir = Vector2.zero;
if(isMove)
{
//设置方向
SetDir();
//产生移动
SetMove();
//相机跟随
SetCam();
}
}
private void SetDir()
{
float angle = Vector2.SignedAngle(Dir, new Vector2(0, 1));//当前的朝向的角度是相当于z轴计算的
Vector3 eulerAngles = new Vector3(0, angle, 0);
transform.localEulerAngles = eulerAngles;
}
private void SetMove()
{
ctrl.Move(transform.forward * Time.deltaTime * Constants.PlayerMoveSpeed);
}
private void SetCam()
{
if(camTrans!=null)
{
camTrans.position = transform.position - camOffset;
}
}
}
速度在Constants中设置
//角色移动速度
public const int PlayerMoveSpeed = 8;
public const int MonsterMoveSpeed = 4;
平滑动画过渡
为了使动画柔和,需要给Blend进行一个平滑过渡的机制
首先在Constants中设置一些属性
//混合参数
public const int BlendIdle = 0;
public const int BlendWalk = 1;
//运动平滑加速度
public const float AccelerSpeed = 5;
然后修改PlayerController中的一些方法
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Transform camTrans;
private Vector3 camOffset;
public Animator ani;
public CharacterController ctrl;
private Vector2 dir = Vector2.zero;
public Vector2 Dir
{
get
{
return dir;
}
set
{
if (value == Vector2.zero)
isMove = false;
else
isMove = true;
dir = value;
}
}
private bool isMove = false;
private float targetBlend;
private float currentBlend;
private void Start()
{
camTrans = Camera.main.transform;
camOffset = transform.position - camTrans.position;
}
private void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
Vector2 _dir = new Vector2(h, v).normalized;
if (_dir != Vector2.zero)
{
Dir = _dir;
SetBlend(Constants.BlendWalk);
}
else
{
Dir = Vector2.zero;
SetBlend(Constants.BlendIdle);
}
if(currentBlend!=targetBlend)
UpdateMixBlend();
if(isMove)
{
//设置方向
SetDir();
//产生移动
SetMove();
//相机跟随
SetCam();
}
}
private void SetDir()
{
float angle = Vector2.SignedAngle(Dir, new Vector2(0, 1));//当前的朝向的角度是相当于z轴计算的
Vector3 eulerAngles = new Vector3(0, angle, 0);
transform.localEulerAngles = eulerAngles;
}
private void SetMove()
{
ctrl.Move(transform.forward * Time.deltaTime * Constants.PlayerMoveSpeed);
}
private void SetCam()
{
if(camTrans!=null)
{
camTrans.position = transform.position - camOffset;
}
}
private void SetBlend(float blend)
{
targetBlend = blend;
}
private void UpdateMixBlend()
{
if(Mathf.Abs(currentBlend - targetBlend)<Constants.AccelerSpeed*Time.deltaTime)
{
currentBlend = targetBlend;
}
else if(currentBlend>targetBlend)
{
currentBlend -= Constants.AccelerSpeed * Time.deltaTime;
}
else
{
currentBlend += Constants.AccelerSpeed * Time.deltaTime;
}
ani.SetFloat("Blend", currentBlend);
}
}
生成与解析主城配置数据
导入一个xml文件
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<item ID="10000">
<mapName>圣光主城</mapName>
<sceneName>SceneMainCity</sceneName>
<mainCamPos>17.4,7,50</mainCamPos>
<mainCamRote>45,135,0</mainCamRote>
<playerBornPos>22,-1.1,45</playerBornPos>
<playerBornRote>0,0,0</playerBornRote>
</item>
</root>
在PathDefine中新建xml文件的地址
public const string MapCfg = "ResCfgs/map";
生成一个新的类,用到接收xml解析出来的信息
using UnityEngine;
/// <summary>
/// 配置数据类
/// </summary>
///
public class MapCfg:BaseData<MapCfg>
{
public string mapName;
public string sceneName;
public Vector3 mainCamPos;
public Vector3 mainCamRote;
public Vector3 playerBornPos;
public Vector3 playerBornRote;
}
public class BaseData<T>
{
public int ID;
}
在ResSvc中添加解析地图xml的代码
private Dictionary<int, MapCfg> mapCfgDataDic = new Dictionary<int, MapCfg>();
private void InitMapCfg(string path)
{
TextAsset xml = Resources.Load<TextAsset>(path);
if (!xml)
Debug.LogError("xml file:" + path + "not exist");
else
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml.text);
XmlNodeList nodLst = doc.SelectSingleNode("root").ChildNodes;
for (int i = 0; i < nodLst.Count; i++)
{
XmlElement ele = nodLst[i] as XmlElement;
if (ele.GetAttributeNode("ID") == null)
continue;
int ID = Convert.ToInt32(ele.GetAttributeNode("ID").InnerText);
MapCfg mc = new MapCfg
{
ID = ID
};
foreach (XmlElement e in nodLst[i].ChildNodes)
{
switch (e.Name)
{
case "mapName":
mc.mapName = e.InnerText;
break;
case "sceneName":
mc.sceneName = e.InnerText;
break;
case "mainCamPos":
{
string[] valArr = e.InnerText.Split(',');
mc.mainCamPos = new Vector3(float.Parse(valArr[0]), float.Parse(valArr[1]), float.Parse(valArr[2]));
}
break;
case "mainCamRote":
{
string[] valArr = e.InnerText.Split(',');
mc.mainCamRote = new Vector3(float.Parse(valArr[0]), float.Parse(valArr[1]), float.Parse(valArr[2]));
}
break;
case "playerBornPos":
{
string[] valArr = e.InnerText.Split(',');
mc.playerBornPos = new Vector3(float.Parse(valArr[0]), float.Parse(valArr[1]), float.Parse(valArr[2]));
}
break;
case "playerBornRote":
{
string[] valArr = e.InnerText.Split(',');
mc.playerBornRote = new Vector3(float.Parse(valArr[0]), float.Parse(valArr[1]), float.Parse(valArr[2]));
}
break;
}
}
mapCfgDataDic.Add(ID, mc);
}
}
}
public MapCfg GetMapCfgData(int id)
{
MapCfg data;
if (mapCfgDataDic.TryGetValue(id, out data))
return data;
return null;
}
加载主角到主城
首先修改Constants中的场景名称为ID
public const int MainCityMapID = 10000;
//public const string SceneMainCity = "SceneMainCity";
然后在ResSvc中添加加载预制体的方法
private Dictionary<string, GameObject> goDic = new Dictionary<string, GameObject>();
public GameObject LoadPrefab(string path, bool cache = false)
{
GameObject prefab = null;
if (!goDic.TryGetValue(path, out prefab))
{
prefab = Resources.Load<GameObject>(path);
if(cache)
{
goDic.Add(path, prefab);
}
}
GameObject go = null;
if (prefab != null)
go = Instantiate(prefab);
return go;
}
添加player的加载地址
public const string AssissnCityPlayerPrefab = "PrefabPlayer/AssassinCity";
修改和添加MainCitySys中的方法
public void EnterMainCity()
{
MapCfg mapData = resSvc.GetMapCfgData(Constants.MainCityMapID);
resSvc.AsyncLoadScene(mapData.sceneName,()=>
{
PECommon.Log("Enter MainCity...");
//加载游戏主角
LoadPlayer(mapData);
//打开主城场景UI
mainCityWnd.SetWndState();
//播放主城里背景音乐
audioSvc.PlayBGMusic(Constants.BGMainCity);
//TODO 设置人物展示相机
});
}
private void LoadPlayer(MapCfg mapData)
{
GameObject player = resSvc.LoadPrefab(PathDefine.AssissnCityPlayerPrefab);
player.transform.position = mapData.playerBornPos;
player.transform.localEulerAngles = mapData.playerBornRote;
player.transform.localScale = new Vector3(1.5f, 1.5f, 1.5f);
//相机初始化
Camera.main.transform.position = mapData.mainCamPos;
Camera.main.transform.localEulerAngles = mapData.mainCamRote;
}
主城碰撞体制作
大部分是有美工来完成,这里直接使用导入好的文件。
方向输入对接
首先添加一个PlayerController,然后对其进行初始化
private PlayerController playerCtrl;
在LoadPlayer里面进行初始化
playerCtrl = player.GetComponent<PlayerController>();
playerCtrl.Init();
在注册点击事件中修改方法,让其能完成方向对接
OnClickUp(imgTouch.gameObject, (PointerEventData evt) => {
imgDirBg.transform.position = defaultPos;
SetActive(imgDirPoint, false);
imgDirPoint.transform.localPosition = Vector2.zero;
MainCitySys.Instance.SetMoveDir(Vector2.zero);
});
OnDrag(imgTouch.gameObject, (PointerEventData evt) => {
Vector2 dir = evt.position - startPos;
float len = dir.magnitude;
if (len > pointDis) {
Vector2 clampDir = Vector2.ClampMagnitude(dir, pointDis);
imgDirPoint.transform.position = startPos + clampDir;
}
else {
imgDirPoint.transform.position = evt.position;
}
MainCitySys.Instance.SetMoveDir(dir.normalized);
});
其中SetMoveDir的方法如下
public void SetMoveDir(Vector2 dir) {
if (dir == Vector2.zero) {
playerCtrl.SetBlend(Constants.BlendIdle);
}
else {
playerCtrl.SetBlend(Constants.BlendWalk);
}
playerCtrl.Dir = dir;
}
在PlayerController还需要注意真实移动方向和照相机角度的问题
float angle = Vector2.SignedAngle(Dir, new Vector2(0, 1)) + camTrans.eulerAngles.y;
第6章: 角色展示系统与任务引导系统
角色信息界面制作
常规信息显示
建立一个InfoWnd的脚本,添加更新UI的方法
using PEProtocol;
using UnityEngine;
using UnityEngine.UI;
public class InfoWnd : WindowRoot
{
#region UI Define
public Text txtInfo;
public Text txtExp;
public Image imgExpPrg;
public Text txtPower;
public Image imgPowerPrg;
public Text txtJob;
public Text txtFight;
public Text txtHp;
public Text txtHurt;
public Text txtDef;
#endregion
protected override void InitWnd()
{
base.InitWnd();
RefreshUI();
}
private void RefreshUI()
{
PlayerData pd = GameRoot.Instance.PlayerData;
SetText(txtInfo, pd.name + " LV." + pd.lv);
SetText(txtExp, pd.exp + "/" + PECommon.GetExpUpValByLv(pd.lv));
imgExpPrg.fillAmount = pd.exp * 1.0f / PECommon.GetExpUpValByLv(pd.lv);
SetText(txtPower, pd.power + "/" + PECommon.GetPowerLimit(pd.lv));
imgPowerPrg.fillAmount = pd.power * 1.0f / PECommon.GetPowerLimit(pd.lv);
SetText(txtJob, " 职业 暗影刺客");
SetText(txtFight, " 战力 " + PECommon.GetFightByProps(pd));
SetText(txtHp, " 血量 " + pd.hp);
SetText(txtHurt, " 伤害 " + (pd.ad + pd.ap));
SetText(txtDef, " 防御 " + (pd.addef + pd.apdef));
//detail TODO
}
}
在MainCitySys中进行封装,在MainCityWnd中进行调用
public void OpenInfoWnd()
{
infoWnd.SetWndState();
}
public void ClickHeadBtn()
{
audioSvc.PlayUIAudio(Constants.UIOpenPage);
MainCitySys.Instance.OpenInfoWnd();
}
角色与UI混合显示
在MainCity的场景中添加一个相机,用来拍摄主角并将其显示在信息中
在MainCitySys中添加部分代码,让我的信息中显示角色
private Transform charCamTrans;
在EnterCity中添加下面的代码
//设置人物展示相机
if(charCamTrans!=null)
{
charCamTrans.gameObject.SetActive(false);
}
在OpenInfoWnd中添加下面的代码
if(charCamTrans==null)
{
charCamTrans = GameObject.FindGameObjectWithTag("CharShowCam").transform;
}
//设置人物展示相机的相对位置
charCamTrans.localPosition = playerCtrl.transform.position + playerCtrl.transform.forward * 3.8f + new Vector3(0, 1.2f, 0);
charCamTrans.localEulerAngles = new Vector3(0, 180 + playerCtrl.transform.localEulerAngles.y, 0);
charCamTrans.localScale = Vector3.one;
charCamTrans.gameObject.SetActive(true);
实现角色旋转触控
InfoWnd里面添加下面的代码,让在鼠标x轴的滑动对应人物在y轴上的转动
private Vector2 startPos;
private void RegTouchEvts()
{
OnClickDown(imgChar.gameObject,(PointerEventData evt)=>
{
startPos = evt.position;
MainCitySys.Instance.SetStartRotate();
});
OnDrag(imgChar.gameObject, (PointerEventData evt) =>
{
float rotate = -(evt.position.x - startPos.x) * 0.4f;
MainCitySys.Instance.SetPlayerRotate(rotate);
});
}
其中
private float startRotate = 0;
public void SetStartRotate()
{
startRotate = playerCtrl.transform.localEulerAngles.y;
}
public void SetPlayerRotate(float rotate)
{
playerCtrl.transform.localEulerAngles = new Vector3(0, startRotate + rotate, 0);
}
详细属性显示
首先创建一个详细属性显示的界面
然后在InfoWnd里面进行代码的添加
public Button btnDetail;
public Button btnClose;
public Button btnCloseDetail;
public Transform transDetail;
public Text dtxhp;
public Text dtxad;
public Text dtxap;
public Text dtxaddef;
public Text dtxapdef;
public Text dtxdodge;
public Text dtxpierce;
public Text dtxcritical;
然后在RefreshUI中进行数据的显示
SetText(dtxhp, pd.hp);
SetText(dtxad, pd.ad);
SetText(dtxap, pd.ap);
SetText(dtxaddef, pd.addef);
SetText(dtxapdef, pd.apdef);
SetText(dtxdodge, pd.dodge+"%");
SetText(dtxpierce, pd.pierce + "%");
SetText(dtxcritical, pd.critical + "%");
按钮的打开与关闭
public void ClickDetailBtn()
{
audioSvc.PlayUIAudio(Constants.UIClickBtn);
SetActive(transDetail);
}
public void ClickCloseBtnDetail()
{
audioSvc.PlayUIAudio(Constants.UIClickBtn);
SetActive(transDetail, false);
}
任务引导系统开发思路
将xls文件导出成xml
任务引导系统配置数据
和之前解析随机名字和场景一样的来解析任务配置数据
public class AutoGuideCfg:BaseData<AutoGuideCfg>
{
public int npcID;//触发任务NPC索引号
public string dilogArr;
public int actID;
public int coin;
public int exp;
}
private Dictionary<int, AutoGuideCfg> guideTaskDic = new Dictionary<int, AutoGuideCfg>();
private void InitGuideCfg(string path)
{
TextAsset xml = Resources.Load<TextAsset>(path);
if (!xml)
{
PECommon.Log("xml file:" + path + " not exist", LogType.Error);
}
else
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml.text);
XmlNodeList nodLst = doc.SelectSingleNode("root").ChildNodes;
for (int i = 0; i < nodLst.Count; i++)
{
XmlElement ele = nodLst[i] as XmlElement;
if (ele.GetAttributeNode("ID") == null)
{
continue;
}
int ID = Convert.ToInt32(ele.GetAttributeNode("ID").InnerText);
AutoGuideCfg mc = new AutoGuideCfg
{
ID = ID
};
foreach (XmlElement e in nodLst[i].ChildNodes)
{
switch (e.Name)
{
case "npcID":
mc.npcID = int.Parse(e.InnerText);
break;
case "diloagArr":
mc.dilogArr = e.InnerText;
break;
case "actID":
mc.npcID = int.Parse(e.InnerText);
break;
case "coin":
mc.coin = int.Parse(e.InnerText);
break;
case "exp":
mc.exp = int.Parse(e.InnerText);
break;
}
}
guideTaskDic.Add(ID, mc);
}
}
}
public AutoGuideCfg GetAutoGuideData(int id)
{
AutoGuideCfg agc = null;
if (guideTaskDic.TryGetValue(id, out agc))
return agc;
return null;
}
任务引导系统开发1
这节首先先在数据库的表中添加一个guideid的属性,用来表示当前正在进行的任务。
然后在服务器中修改各类增删改查数据库的操作,把guideid添加进去。
在客户端中,我们需要定义任务图标的路径和id,然后把它们设置到自动任务的图标上去。
public const string TaskHead = "ResImages/task";
public const string WiseManHead = "ResImages/wiseman";
public const string GeneralHead = "ResImages/general";
public const string ArtisanHead = "ResImages/artisan";
public const string TraderHead = "ResImages/trader";
//AutoGuideNpc
public const int NPCWiseMan = 0;
public const int NPCGeneral = 1;
public const int NPCArtisan = 2;
public const int NPCTrader = 3;
在MainCityWnd中写入新的方法
private AutoGuideCfg curtTaskData;
public Button btnGuide;
private void SetGuideBtnIcon(int npcID)
{
string spPath = "";
Image image = btnGuide.GetComponent<Image>();
switch(npcID)
{
case Constants.NPCWiseMan:
spPath = PathDefine.WiseManHead;
break;
case Constants.NPCGeneral:
spPath = PathDefine.GeneralHead;
break;
case Constants.NPCArtisan:
spPath = PathDefine.ArtisanHead;
break;
case Constants.NPCTrader:
spPath = PathDefine.TraderHead;
break;
}
}
在RefreshUI中进行调用
//设置自动任务图标
curtTaskData = resSvc.GetAutoGuideData(pd.guideid);
if(curtTaskData != null)
{
SetGuideBtnIcon(curtTaskData.npcID);
}
else
{
SetGuideBtnIcon(-1);
}
任务引导系统开发2
封装通过路径加载图片的方法在ResSvc中
private Dictionary<string, Sprite> spDic = new Dictionary<string, Sprite>();
public Sprite LoadSprite(string path,bool cache = false)
{
Sprite sp = null;
if(!spDic.TryGetValue(path,out sp))
{
sp = Resources.Load<Sprite>(path);
if (cache)
{
spDic.Add(path, sp);
}
}
return sp;
}
然后在WindowRoot中完成设置图片的方法
protected void SetSprite(Image img,string path)
{
Sprite sp = resSvc.LoadSprite(path,true);
img.sprite = sp;
}
最后在之前的SetGuideBtnIcon的方法中,完成最后一步赋值
SetSprite(image, spPath);
因为默认是1001白魔法师的图片
任务引导系统开发3
首先给Guide按钮添加点击事件
public void ClickGuideBtn()
{
audioSvc.PlayUIAudio(Constants.UIClickBtn);
if(curtTaskData!=null)
{
MainCitySys.Instance.RunTask(curtTaskData);
}
else
{
GameRoot.AddTips("更多引导任务,正在开发中...");
}
}
然后在MainCitySys中进行任务处理
private AutoGuideCfg curtTaskData;
public void RunTask(AutoGuideCfg agc)
{
if (agc != null)
{
curtTaskData = agc;
}
//解析任务数据
if(curtTaskData.npcID != -1)
{
}
else
{
OpenGuideWnd();
}
}
private void OpenGuideWnd()
{
//TODO
}
任务引导系统开发4
首先在主城中创建好各个npc,然后每个npc创建一个空物体,用它的坐标来表示玩家和npc交互的位置。
建立一个MainCityMap脚本,用来存储npc交互的位置
using UnityEngine;
public class MainCityMap : MonoBehaviour
{
public Transform[] NpcPosTrans;
}
然后在MainCitySys中进行使用,修改EnterMainCity方法
private Transform[] npcPosTrans;
//加载NPC的位置
GameObject map = GameObject.FindGameObjectWithTag("MapRoot");
MainCityMap mcm = map.GetComponent<MainCityMap>();
npcPosTrans = mcm.NpcPosTrans;
任务引导系统开发5
使用Navigation烘焙自动寻路的路径,然后在角色身上挂上navigation的脚本。
平常使用Character Controller进行移动,导航时激活Nav Mesh Agent进行移动。
任务引导系统开发6
在MainCitySys中添加关于导航控制的代码,首先先拿到挂载的组件
private NavMeshAgent nav;
nav = player.GetComponent<NavMeshAgent>();
然后添加方法
private bool isNavGuide = false;
public void RunTask(AutoGuideCfg agc)
{
if (agc != null)
{
curtTaskData = agc;
}
//解析任务数据
if(curtTaskData.npcID != -1)
{
float dis = Vector3.Distance(playerCtrl.transform.position, npcPosTrans[agc.npcID].position);
if(dis<0.5f)//停止导航
{
isNavGuide = false;
nav.isStopped = true;
nav.enabled = false;
playerCtrl.SetBlend(Constants.BlendIdle);
OpenGuideWnd();
}
else//开始导航
{
isNavGuide = true;
nav.enabled = true;
nav.speed = Constants.PlayerMoveSpeed;
nav.SetDestination(npcPosTrans[agc.npcID].position);
playerCtrl.SetBlend(Constants.BlendWalk);
}
}
else
{
OpenGuideWnd();
}
}
任务引导系统开发7
完善导航系统
private bool isNavGuide = false;
public void RunTask(AutoGuideCfg agc)
{
if (agc != null)
{
curtTaskData = agc;
}
nav.enabled = true;
//解析任务数据
if(curtTaskData.npcID != -1)
{
float dis = Vector3.Distance(playerCtrl.transform.position, npcPosTrans[agc.npcID].position);
if(dis<0.5f)//停止导航
{
isNavGuide = false;
nav.isStopped = true;
nav.enabled = false;
playerCtrl.SetBlend(Constants.BlendIdle);
Debug.Log("stop");
OpenGuideWnd();
}
else//开始导航
{
isNavGuide = true;
nav.enabled = true;
nav.speed = Constants.PlayerMoveSpeed;
nav.SetDestination(npcPosTrans[agc.npcID].position);
playerCtrl.SetBlend(Constants.BlendWalk);
}
}
else
{
OpenGuideWnd();
}
}
private void IsArriveNavPos()
{
float dis = Vector3.Distance(playerCtrl.transform.position, npcPosTrans[curtTaskData.npcID].position);
if (dis < 0.5f)//停止导航
{
isNavGuide = false;
nav.isStopped = true;
nav.enabled = false;
playerCtrl.SetBlend(Constants.BlendIdle);
Debug.Log("stop");
OpenGuideWnd();
}
}
private void Update()
{
if (isNavGuide)
{
IsArriveNavPos();
playerCtrl.SetCam();
}
}
private void StopNavTask()
{
if(isNavGuide)
{
isNavGuide = false;
nav.isStopped = true;
nav.enabled = false;
playerCtrl.SetBlend(Constants.BlendIdle);
}
}
在自己控制人物移动和打开界面的时候调用StopNavTask()方法,停止导航。
NPC对话界面1
完成GuideWnd的设计,给它添加同名脚本,并在MainCitySys中进行调用
private void OpenGuideWnd()
{
guideWnd.SetWndState();
}
NPC对话界面2
首先把在PathDefine中,定义好各个NPC的图片位置
public const string SelfIcon = "ResImages/assassin";
public const string GuideIcon = "ResImages/npcguide";
public const string WiseManIcon = "ResImages/npc0";
public const string GenralIcon = "ResImages/npc1";
public const string AretisanIcon = "ResImages/npc2";
public const string TraderIcon = "ResImages/npc3";
然后在GuideWnd中完成对话功能
public class GuideWnd : WindowRoot
{
public Text txtName;
public Text txtTalk;
public Image imgIcon;
private PlayerData pd;
private AutoGuideCfg curtTaskData;
private string[] dilogArr;
private int index;
protected override void InitWnd()
{
base.InitWnd();
pd = GameRoot.Instance.PlayerData;
curtTaskData = MainCitySys.Instance.GetCurtTaskData();
dilogArr = curtTaskData.dilogArr.Split('#');
index = 1;
SetTalk();
}
private void SetTalk()
{
string[] talkArr = dilogArr[index].Split('|');
if(talkArr[0]=="0")
{
//自己
SetSprite(imgIcon,PathDefine.SelfIcon);
SetText(txtName,pd.name);
}
else
{
//对话NPC
switch(curtTaskData.npcID)
{
case 0:
SetSprite(imgIcon, PathDefine.WiseManIcon);
SetText(txtName, "智者");
break;
case 1:
SetSprite(imgIcon, PathDefine.GenralIcon);
SetText(txtName, "将军");
break;
case 2:
SetSprite(imgIcon, PathDefine.AretisanIcon);
SetText(txtName, "工匠");
break;
case 3:
SetSprite(imgIcon, PathDefine.TraderIcon);
SetText(txtName, "商人");
break;
default:
SetSprite(imgIcon, PathDefine.GuideIcon);
SetText(txtName, "真夏糖");
break;
}
}
imgIcon.SetNativeSize();
SetText(txtTalk, talkArr[1].Replace("$name", pd.name));
}
public void ClickNextBtn()
{
audioSvc.PlayUIAudio(Constants.UIClickBtn);
index += 1;
if(index == dilogArr.Length)
{
//TODO 发送引导任务完成信息
SetWndState(false);
}
else
{
SetTalk();
}
}
}
定制引导数据网络协议
在服务器端的GameMsg中添加引导相关的属性
[Serializable]
public class ReqGuide
{
public int guideid;
}
[Serializable]
public class RspGuide
{
public int guideid;
public int coin;
public int lv;
public int exp;
}
给引导相关的属性添加CMD号
//主城相关 200
ReqGuide = 200,
RspGuide = 201,
在GameMsg类中添加这两个属性
[Serializable]
public class GameMsg : PEMsg {
public ReqLogin reqLogin;
public RspLogin rspLogin;
public ReqRename reqRename;
public RspRename rspRename;
public ReqGuide reqGuide;
public RspGuide rspGuide;
}
从客户端向服务器端发送ReqGuide
//发送引导任务完成信息
GameMsg msg = new GameMsg
{
cmd = (int)CMD.ReqGuide,
reqGuide = new ReqGuide
{
guideid = curtTaskData.ID
}
};
netSvc.SendMsg(msg);
引导数据网络协议处理
服务器端创建GuideSys用来处理引导信息
//引导业务系统
using PEProtocol;
class GuideSys
{
private static GuideSys instance = null;
public static GuideSys Instance
{
get
{
if (instance == null)
{
instance = new GuideSys();
}
return instance;
}
}
private CacheSvc cacheSvc = null;
public void Init()
{
cacheSvc = CacheSvc.Instance;
PECommon.Log("GuideSysInit Done.");
}
public void ReqGuide(MsgPack pack)
{
ReqGuide data = pack.msg.reqGuide;
GameMsg msg = new GameMsg
{
cmd = (int)CMD.RspGuide
};
PlayerData pd = cacheSvc.GetPlayerDataBySession(pack.session);
//更新引导ID
if(pd.guideid == data.guideid)
{
pd.guideid += 1;
//更新玩家数据
}
else
{
msg.err = (int)ErrorCode.ServerDataError;
}
//发送回客户端
pack.session.SendMsg(msg);
}
}
增加服务器数据配置服务
服务器端创建CfgSvc,用来从服务器端读取引导信息
//配置数据服务
using System;
using System.Collections.Generic;
using System.Xml;
public class CfgSvc
{
private static CfgSvc instance = null;
public static CfgSvc Instance
{
get
{
if (instance == null)
{
instance = new CfgSvc();
}
return instance;
}
}
public void Init()
{
InitGuideCfg();
PECommon.Log("CfgSvc Init Done.");
}
#region 自动引导服务
private Dictionary<int, GuideCfg> guideDic = new Dictionary<int, GuideCfg>();
private void InitGuideCfg()
{
XmlDocument doc = new XmlDocument();
doc.Load(@"D:\u3dworkspace\DarkGod_第五章\Client\Assets\Resources\ResCfgs\guide.xml");
XmlNodeList nodLst = doc.SelectSingleNode("root").ChildNodes;
for (int i = 0; i < nodLst.Count; i++)
{
XmlElement ele = nodLst[i] as XmlElement;
if (ele.GetAttributeNode("ID") == null)
{
continue;
}
int ID = Convert.ToInt32(ele.GetAttributeNode("ID").InnerText);
GuideCfg mc = new GuideCfg
{
ID = ID
};
foreach (XmlElement e in nodLst[i].ChildNodes)
{
switch (e.Name)
{
case "coin":
mc.coin= int.Parse(e.InnerText);
break;
case "exp":
mc.exp = int.Parse(e.InnerText);
break;
}
}
guideDic.Add(ID, mc);
}
}
public GuideCfg GetGuideData(int id)
{
GuideCfg agc = null;
if (guideDic.TryGetValue(id, out agc))
return agc;
return null;
}
}
#endregion
public class GuideCfg : BaseData<GuideCfg>
{
public int coin;
public int exp;
}
public class BaseData<T>
{
public int ID;
}
经验升级计算
添加一个计算经验值和升级的方法
private void CalcExp(PlayerData pd,int addExp)
{
int curtLv = pd.lv;
int curtExp = pd.exp;
int addRestExp = addExp;
while(true)
{
int upNeedExp = PECommon.GetExpUpValByLv(curtLv) - curtExp;
if(addRestExp>=upNeedExp)
{
curtLv += 1;
curtExp = 0;
addRestExp -= upNeedExp;
}
else
{
pd.lv = curtLv;
pd.exp = curtExp + addRestExp;
break;
}
}
}
完善处理ReqGuide的方法
public void ReqGuide(MsgPack pack)
{
ReqGuide data = pack.msg.reqGuide;
GameMsg msg = new GameMsg
{
cmd = (int)CMD.RspGuide
};
PlayerData pd = cacheSvc.GetPlayerDataBySession(pack.session);
GuideCfg gc = cfgSvc.GetGuideData(data.guideid);
//更新引导ID
if(pd.guideid == data.guideid)
{
pd.guideid += 1;
//更新玩家数据
pd.coin += gc.coin;
CalcExp(pd, gc.exp);
if(!cacheSvc.UpdatePlayerData(pd.id,pd))
{
msg.err = (int)ErrorCode.UpdateDBError;
}
else
{
msg.rspGuide = new RspGuide
{
guideid = pd.guideid,
coin = pd.coin,
lv = pd.lv,
exp = pd.exp
};
}
}
else
{
msg.err = (int)ErrorCode.ServerDataError;
}
//发送回客户端
pack.session.SendMsg(msg);
}
更新客户端任务
在客户端MainCitySys中处理RspGuide
public void RspGuide(GameMsg msg)
{
RspGuide data = msg.rspGuide;
GameRoot.AddTips("任务奖励 金币+" + curtTaskData.coin+" 经验+"+curtTaskData.exp);
switch(curtTaskData.actID)
{
case 0:
//与智者对话
break;
case 1:
//进入副本
break;
case 2:
//强化界面
break;
case 3:
//体力购买
break;
case 4:
//金币铸造
break;
case 5:
//世界聊天
break;
}
GameRoot.Instance.SetPlayerDataByGuide(data);
maincityWnd.RefreshUI();
}
其中SetPlayerDataByGuide()
public void SetPlayerDataByGuide(RspGuide data)
{
PlayerData.coin = data.coin;
playerData.exp = data.exp;
playerData.lv = data.lv;
playerData.guideid = data.guideid;
}
增加文字染色工具
在Constants中添加颜色和染色工具
public enum TxtColor
{
Red,
Green,
Blue,
Yellow
}
private const string ColorRed = "<color=#FF0000FF>";
private const string ColorGreen = "<color=#00FF00FF>";
private const string ColorBlue = "<color=#00B4FFFF>";
private const string ColorYellow = "<color=#FFFF00FF>";
private const string ColorEnd = "</color>";
public static string Color(string str,TxtColor c)
{
string result = "";
switch(c)
{
case TxtColor.Red:
result = ColorRed + str + ColorEnd;
break;
case TxtColor.Green:
result = ColorGreen + str + ColorEnd;
break;
case TxtColor.Blue:
result = ColorBlue + str + ColorEnd;
break;
case TxtColor.Yellow:
result = ColorYellow + str + ColorEnd;
break;
}
return result;
}
在适当的地方进行调用
GameRoot.AddTips(Constants.Color("任务奖励 金币+" + curtTaskData.coin+" 经验+"+curtTaskData.exp,TxtColor.Blue));