主城灯光烘焙

完全烘焙灯光,所有的光源改为“baked”模式,可以通过Light Explorer面板进行确认和设置。

打开Lighting面板,在Environment选项卡中将Environment Lighting的Source改为Color。在Scene选项卡中关闭Realtime Lighting只打开Mixed Lighting,打开烘焙全局光,Lighting Mode选择Subtractive。

在游戏场景的static物体中,玩家经常看到的物体的Mesh Renderer组件内的Lightmapping参数——Scale in Lightmap都设为1,玩家看不到的装饰性物体,Scale in Lightmap都设为0.1或0.2

烘焙参数:

烘焙参数

  • Indirect Resolution:间接光分辨率
  • Lightmap Resolution:灯光贴图分辨率
  • Lightmap padding:灯光贴图的间隙,贴图拼合时的空隙
  • Directional Mode:如果场景材质有法线贴图,打开Directional会烘焙法线

Unity - Manual: Lightmapping using Enlighten Baked Global Illumination

雾效参数:

  • Mode:Linear
  • Start:15
  • End:3035

最后点击Generate Enlighting,等待贴图生成好后,删除场景内的灯光即可

烘焙完成后,会生成一个LightingData,LightingData与使用这个Data的场景相关联,还有一个Lightmap Texture。

计算战力的公式

1
2
3
4
public static int GetFightByProps(PlayerData playerData)
{
return playerData.lv * 100 + playerData.ad + playerData.ap + playerData.addef + playerData.apdef;
}

体力限制计算公式

体力要求:在110级,体力最高为150,在1120级别体力最高为300……

1
2
3
4
public static int GetPowerLimit(int lv)
{
return ((lv - 1) / 10 )* 150 + 150;
}

这里只要我们的lv是int类型的,((lv - 1) / 10 )在lv为1到10时恒为0,整数计算遇到小数是全部舍弃的

如果lv是float类型的,则会进行正常计算

经验条UI

经验条为分段UI,每到10%就涨一个图块。

我们使用Grid Layout Group来排10个图片来实现,为了实现UI自适应,要动态计算Cell Size的x值

Grid Cell Size

经验条计算

我们在Canvas中设置的像素是1920*1080,所以宽度总长是1920

Exp图片的宽度是76,所以经验条部分总长是1920 - 76 = 1844

我们在Grid Layout Group中设置的Padding Left是5,剩下的长度是1844 - 5 = 1839

我们在Spacing设置的X为10,中间的9个间隙的长度就是90,1839 - 90 = 1749

由此可见,如果我们每个图块的长度设置为174,最右侧的空隙为9,如果我们舍去这个空隙,那么图块的最大长度为174.9,再长就要换行了。

图块最大长度 =(1920-76-5-90)/10。

图块美观最大长度 =(1920-76-5-90-9)/10 = (1920 - 180)/10

在实际情况中,1920这个宽度是不固定的,而180这个减小值是固定的,我们按照下面的代码来计算出实际的宽度,进而设置好图块的宽度

1
2
3
4
5
6
7
8
9
10
//获取GridLayoutGroup组件
GridLayoutGroup grid = expPrgTrans.GetComponent<GridLayoutGroup>();
//因为我们在Canvas中将缩放设为完全由height决定,并且值为1920*1080,所以这里使用1080 / Screen.height来得到比率
float globalRate = 1.0f * 1080 / Screen.height;
//根据比率算出canvas内部的canvasWidth
float canvasWidth = Screen.width * globalRate;
//根据上面的推导计算出cellwidth
float cellWidth = (canvasWidth - 180) / 10;

grid.cellSize = new Vector2(cellWidth, 21);

升级公式

接下来计算升级的百分比,首先在PECommon中定义升级公式:

1
2
3
4
public static int GetExpUpValByLv(int lv)
{
return 100 * lv * lv;
}

然后通过计算转化百分比:

1
2
int expPrgVal = (int)((playerData.exp * 1.0f / PECommon.GetExpUpValByLv(playerData.lv)) * 100);//*1.0f属于转化为浮点数的操作
SetText(txtExpPrg,expPrgVal + "%");

经验条计算

首先根据算出来的expPrgVal来获取应该点亮的图块的Index

1
int index = expPrgVal / 10;//小数部分被舍弃

然后根据index设置各个图块的fill amount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (int i = 0; i < expPrgTrans.childCount; i++)
{
Image image = expPrgTrans.GetChild(i).GetComponent<Image>();
if (i < index)
{
image.fillAmount = 1;
}
else if(i == index)
{
image.fillAmount = expPrgVal % 10 * 1.0f / 10;
}
else
{
image.fillAmount = 0;//超过了索引不显示
}
}

下线管理

一个客户端下线,就意味着断掉对应的session,并清理掉cacheSvc里面的缓存。

在ServerSession里面声明一个int变量,来记录当前session的id

1
2
3
4
5
public class ServerSession : PESession<GameMsg>
{
public int sessionID = 0;
//...
}

记住在服务器中,Server Session是多线程多对象的,服务器每与一个客户端建立连接就会创建一个server session

每次有客户端建立连接时,都要有一个函数来创建与当前session连接的唯一id号,我们在ServerRoot里面新建这个程序

1
2
3
4
5
6
private int sessionId = 0;
public int GetSessionID()
{
if(sessionId == int.MinValue) { sessionId = 0; }//如果服务器长时间运行,sessionId必须要重置
return sessionId += 1;
}

修改后的ServerSession如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using PENet;
using PEProtocol;
public class ServerSession : PESession<GameMsg>
{
public int sessionID = 0;
protected override void OnConnected()
{
sessionID = ServerRoot.Instance.GetSessionID();
PECommon.Log("SessionID: " + sessionID + " Client Connected");
}
protected override void OnReciveMsg(GameMsg msg)//多线程
{
PECommon.Log("SessionID: " + sessionID + "RcvPack CMD:" + ((CMD)msg.cmd).ToString());
NetSvc.Instance.AddMsgQue(this,msg);
}
protected override void OnDisConnected()
{
PECommon.Log("SessionID: " + sessionID + "Client DisConnected");
}
}

CacheSvc增加下线逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

我们在LoginSys里面添加ClearOfflieData来做一下中转,因为server Session不应该直接调用cacheSvc,cacheSvc只能被具体的业务系统(system)调用。

1
2
3
4
public void ClearOfflieData(ServerSession session)
{
_cacheSvc.AcctOffLine(session);
}
1
2
3
4
5
protected override void OnDisConnected()
{
LoginSys.Instance.ClearOfflieData(this);
PECommon.Log("SessionID: " + sessionID + "Client DisConnected");
}

心跳机制

服务器每隔一段时间向客户端发送一次消息,如果连续3次没有收到客户端的回应,那么就对这个客户端做下线处理。

角色控制带动画平滑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DarknessWarGodLearning
{
public class PlayerController : MonoBehaviour
{
[SerializeField] Animator playerAnimator;
[SerializeField] CharacterController characterController;
//Constants.AccelerSpeed = 5

private Transform camTrans;
private Vector2 dir = Vector2.zero;
private bool isMove = false;
private Vector3 camOffset;
private float targetBlend;
private float currentBlend;

public Vector2 Dir { get => dir; set
{
if(value == Vector2.zero)
{
isMove = false;
}
else
{
isMove = true;
}
dir = value;
} }

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(1);
}
else
{
Dir = Vector2.zero;
SetBlend(0);
}
if(currentBlend != targetBlend)
{
UpdateMixBlend();
}

if (isMove)
{
//设置方向
SetDir();

//产生移动
SetMove();

//摄像机跟随
SetCam();
}
}
private void SetDir()
{
float angle = Vector2.SignedAngle(Dir, new Vector2(0, 1));
Vector3 eularAngle = new Vector3(0, angle, 0);
transform.localEulerAngles = eularAngle;
}
private void SetMove()
{
characterController.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()
{
//Constants.AccelerSpeed*Time.deltaTime就是currentBlend每一帧应该变化的值
//第一个判断的是currentBlend和targetBlend很接近的情况
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;
}
playerAnimator.SetFloat("Blend", currentBlend);

}
}
}