个人兴趣+项目需要,学习一下Unity引擎,在此记录一下自己所作的小项目。
项目地址:https://github.com/hahahappyboy/UnityProjects
总览
烟花(粒子系统)
热更新(XLuaHotFix)
绘画涂鸦(图像处理、射线检测)
Unity常用框架(对象池框架、状态机框架、UI框架)
视频播放(Lua调UnityAPI)
AB包使用(异步加载AB包)
编辑器模式运行 (Editor编辑器开发)
Phong光照模型(顶点片元Shader、表面体Shader)
人物发光特效(表面体Shader)
图像渐变(固定管线Shader)
商城系统(SQLite访问)
3D塔防(AI寻路)
A*寻路算法
动画系统(动画状态机)
背包系统(物品拖拽)
关卡选择UI界面(UGUI)
3D坦克大战(物理系统)
打砖块(射线检测)
拾取金币(碰撞检测)
行星绕恒星转动
个人场景搭建
滚球游戏(输入输出轴)
注意事项:
代码见https://blog.csdn.net/iiiiiiimp/article/details/130914314
注意事项:
强烈推荐这篇博客https://www.cnblogs.com/gangtie/p/13665727.html,将Xlua的运行机制很透彻。
1、前期准备:
(1)下载XLua,解压后将目录中Assets文件夹下的所有资源复制到Unity工程的Assets文件夹下。
(1)将目录中Tools文件复制到Unity工程与Assets同级的目录下
(2)在PlayerSettings里面添加宏信息HOTFIX_ENABLE,表示支持热更。
(3)把刚刚复制到Unity/Assets/Xlua中的examples文件删除掉并执行顶部菜单栏中的Xlua->Clear Generated Code
2、关键操作的理解
关于顶部菜单中的Xlua->Generate Code:点GenerateCode会在Assets/XLua/Gen文件下生成一些Bridge文件,这些文件是和lua文件通信用到。
关于顶部菜单中的Xlua->Hotfix inject in Editor:是对C#编译的代码进行IL注入,把Lua代码嵌入到里面
所以每当我们修改了C#代码都需要重新Generate Code,然后Hotfix inject in Editor,不然会保存
3、Xlua中关键特性的理解
[HotFix]特性:在类上打上[HotFix]特性,代表你这个类需要后续进行“热补丁修复”的类。
[LuaCallCSharp]特性:表示如果一个C#类型添加了该标签,xLua会生成这个类型的适配代码(包括构造该类型实例,访问其成员属性、方法,静态属性、方法),否则将会尝试用性能较低的反射方式来访问。
[CSharpCallLua]特性:表示如果C#想要访问Lua中函数或Table,就要在C#中对应的Delegate或Interface添加该标签。
所以我们一般会在需要热更新方法的类上打上[HotFix]标签,然后在类中需要热更的方法打上[LuaCallCSharp]标签,如果不打上[LuaCallCSharp]也可以只不过影响性能。需要将Lua中的方法映射为C#的Delegate时就在Delegate上打上[CSharpCallLua]特性
4、xlua热更新函数的理解
xlua.hotfix(CS.类名,‘方法名’,lua方法)
CS.类名表示在C#代码中打[HotFix]标签的类。‘方法名’表示要对该类的哪个方法进行热更。lua方法表示用这个lua的方法替换掉C#中的那个方法。
如下就表示,我需要对C#代码的AssetLoad类的StartGameClick方法进行更新,用function(self)中的方法替换掉C#AssetLoad类的StartGameClick方法5、热更新流程
流程就是先从服务器上下载AB包和Lua脚本,然后用C#执行lua热更新脚本使其替换掉原来的C#脚本中函数,最后才执行游戏
见https://blog.csdn.net/iiiiiiimp/article/details/129590527
对象池框架
可以看到从空中生成小球时不会创建新的GameObjcet而是把之前的小球拿来复用。这样虽然会加大内存的消耗,但是却减少了CPU的调度支援所产生的消耗。
对象池框架关键讲解
使用一个字典去存储物体的Name和GameObjcet
Dictionary<string, List<GameObject>> gameObjectPool;
创建物体时先去判断一些对象池有没有该物体,有的话就直接重新初始化其参数然后拿来用,没有就Instantiate
一个
//生成游戏对象
public GameObject CreatGameObject(string gameObjectName) {
GameObject gameObject = null;
//判断池子里有没有该对象
if (gameObjectPool.ContainsKey(gameObjectName) && gameObjectPool[gameObjectName].Count > 0) {//有
gameObject = gameObjectPool[gameObjectName][0];
gameObject.SetActive(true);
gameObjectPool[gameObjectName].RemoveAt(0);
} else {//没有
Object prefabs = Resources.Load(PREFABS_PATH + gameObjectName);
gameObject = Object.Instantiate(prefabs) as GameObject;
gameObject.name = gameObject.name.Replace("(Clone)", "");
}
gameObject.GetComponent<SphereController>().WhenCreate();
return gameObject;
}
不在使用的物体就直接回收到对象池里面,而不是销毁
//回收游戏对象
public void RecycleGameObject(GameObject gameObject) {
gameObject.SetActive(false);
if (gameObjectPool.ContainsKey(gameObject.name)) {
gameObjectPool[gameObject.name].Add(gameObject);
} else {
gameObjectPool.Add(gameObject.name,new List<GameObject>(){gameObject});
}
gameObject.GetComponent<SphereController>().WhenRecycle();
状态机框架
状态机框架类似于Unity中的动画状态机,只不过是通用的框架。一个状态机中有多个状态也可以包含其他状态机。每个状态机有过渡条件,满足该条件可以从一个状态过渡到另一个状态。
状态机框架关键讲解
(1)在State状态类中,使用 private Dictionary
存储该状态能过渡到的状态名和状态条件。注意是这个状态能过渡到的状态的状态名和状态条件,而不是能过渡到该状态的条件。
在CheckTransition方法中检测检测是否满足某个状态过度的条件,如果满足就返回这个状态的名称
public virtual string CheckTransition() {
foreach (var item in canTransitionStateDic) {
if (item.Value()) {//满足
return item.Key;
}
}
return null;
}
State类中有三个事件public event Action OnStateEnter;
、public event Action OnStateUpdate;
、public event Action OnStateExit;
分别是在进入状态时触发事件、状态执行中时不断触发事件、状态离开时触发事件。因此一定是在OnStateUpdate方法中执行CheckTransition方法,这样才能不断检测有没有可过渡的状态。
//进入状态触发事件
public event Action OnStateEnter;
//状态执行中触发事件
public event Action OnStateUpdate;
//状态离开时触发事件
public event Action OnStateExit;
(2)在StateMachine状态机类中:该类继承了State类,因为状态机类也相当于是状态。使用Dictionary
来存储状态。State defaultState
代表进入状态机默认要运行的状态。State currentState
代表当前状态机正在运行的状态。
CheckCanTransition方法不断检测当前正在运动的状态是否有可过渡的状态,有就过渡。
private void CheckCanTransition()
{
if(currentState == null)
return;
// 不断检测当前状态能过渡的状态有哪些
string canTransitionState = currentState.CheckTransition();
if (canTransitionState != null) {
//过渡到这个状态
TransitionState(canTransitionState);
}
}
(3)UpdateEventTrigger类:该类继承了MonoBehaviour类,这是为了让在Update函数中不断检测当前运行状态是否有可过渡的状态。actionEvents
里面装的就是当前运行状态的OnStateUpdate函数。
void Update() {
for (int i = 0; i < actionEvents.Length; i++)
{
//执行事件
actionEvents[i]();
}
}
当前运行状态的OnStateUpdate函数在进入该状态时就绑定到了actionEvents
上
//进入该状态调用的函数
public virtual void EnterState() {
//执行进入事件
OnStateEnter();
//TODO:与触发器绑定,进入跟新状态
UpdateEventTrigger.GetInstance().AddUpdateEvent(stateName,OnStateUpdate);
}
(4)最后在Demo类中为状态和状态机添加过渡条件,然后执行即可。
UI框架
类似于Android使用栈去管理Activity一样。当一个界面显示了就进栈,前一个界面就被压下去,前一个界面的所有点击事件将不可用。当一个界面退出时就出栈。我们在Unity中使用一样的思路,去管理UI界面。
UI框架关键讲解:
(1)UIModuleBase类作为所有界面的基类,使用[RequireComponent(typeof(CanvasGroup))]
绑定CanvasGroup组件。CanvasGroup组件可以设置是否阻挡鼠标射线继续向下发射,从而实现当某个界面显示在最上层时,被遮挡的UI都无法显示。
//第一次加载该界面,显示在最上面时
public virtual void UIEnter() {
_canvasGroup.blocksRaycasts = true;
_canvasGroup.alpha = 1;
}
//当前界面被其他界面遮挡时
public virtual void UIPause() {
_canvasGroup.blocksRaycasts = false;
_canvasGroup.alpha = 0.5f;
}
//其他界面退出,该页面处于最上面时
public virtual void UIResume() {
_canvasGroup.blocksRaycasts = true;
_canvasGroup.alpha = 1;
}
public virtual void UIExit() {
_canvasGroup.blocksRaycasts = false;
_canvasGroup.alpha = 0;
}
(2)UIManager类中使用 private Stack
存UI界面的UIModuleBase组件从而实现UI界面的进栈出栈。
//界面压栈
public void PushUI(string uiName) {
//先让本来在栈最上面的ui给暂停
if (_uiModuleStack.Count > 0) {
_uiModuleStack.Peek().UIPause();
}
//没有该生成过该界面,就生成
if (!uiNameGameObjectDic.ContainsKey(uiName)) {
GameObject uiPrefab = GetUIGameObject(uiName);
//压栈
_uiModuleStack.Push(uiPrefab.GetComponent<UIModuleBase>());
}
//执行进入触发事件
_uiModuleStack.Peek().UIEnter();
}
//界面出栈
public void PopUI() {
if (_uiModuleStack.Count>0) {
//当前模块离开
_uiModuleStack.Peek().UIExit();
_uiModuleStack.Pop();
if (_uiModuleStack.Count>0) {
_uiModuleStack.Peek().UIResume();
}
}
}
注意事项:
(1)XLua使用了单例模式和自定义Loader,自定义Loader是为了重新定位Lua文件的路径,因为默认的路径是StreamingAssets目录。自定义Load函数参数和返回值是固定的写法private byte[] CustomLoader(ref string filePath)
。之后使用DoString()
函数调用lua语句。
public class XluaEnv {
//单例
private static XluaEnv _Instance = null;
public static XluaEnv Instance {
get {
if (_Instance == null) {
_Instance = new XluaEnv();
}
return _Instance;
}
}
private LuaEnv _luaEnv;
private XluaEnv() {
_luaEnv = new LuaEnv();
_luaEnv.AddLoader(CustomLoader);
}
//自定义Loader
private byte[] CustomLoader(ref string filePath) {
string path = Application.dataPath;
path = path.Substring(0,path.Length-7) + "/DataPath/Lua/" + filePath + ".lua";
Debug.Log(path);
if (File.Exists(path)) {
return File.ReadAllBytes(path);
} else {
return null;
}
}
//调用Lua
public object[] DoString(string code) {
return _luaEnv.DoString(code);
}
//释放
public void Free() {
_luaEnv.Dispose();
_Instance = null;
}
//获取Lua中的全局变量
public LuaTable Global {
get {
return _luaEnv.Global;
}
}
}
(2)在启动脚本Bootstrap.cs
使用XluaEnv.Instance.DoString("require('Bootstrap')");
执行lua的启动脚本Bootstrap.lua
使用 _luaBootstrap = XluaEnv.Instance.Global.Get
将Bootstrap.lua
中的BootStrap
表映射到自定义的Unity中的结构体LuaBootstrap
上,并且在Start()
和Update()
方法中分别调用结构体的委托Start()
和Update()
方法,这样就实现了lua的生命周期。
// lua表映射
[CSharpCallLua]
public delegate void LifeCycle();
[GCOptimize()]
public class LuaBootstrap {
public LifeCycle Start;
public LifeCycle Update;
}
public class Bootstrap : MonoBehaviour {
// private GameObject _button;
//lua得Boosstrap
public LuaBootstrap _luaBootstrap;
private void Start() {
//防止切换场景时,脚本对象丢失
DontDestroyOnLoad(gameObject);
XluaEnv.Instance.DoString("require('Bootstrap')");
_luaBootstrap = XluaEnv.Instance.Global.Get<LuaBootstrap>("BootStrap");
_luaBootstrap.Start();
}
private void Update() {
_luaBootstrap.Update();
}
}
(3)lua中主要就是调用Unity中的API,记得使用其他lua文件变量时要require()
加载一下这个lua文件。由于lua没有泛型,所以调UnityAPI的时候,一般会用其对应的太有Type参数的重载方法。例如加载AB包时的这段代码ABManager.Manifest = mainAssetBundle:LoadAsset("AssetBundleManifest", typeof(CS.UnityEngine.AssetBundleManifest))
。lua写法其实和Unity差不多,规则就是CS.命名空间.对应变量。这里展示该项目UI界面的写法
--[[UI界面]]
UIManager = {}
function UIManager:Start()
-- print('ui_manager:Start')
ABManager:LoadFile("prefabs")
local buttonPrefab = ABManager:LoadAsset("prefabs","Button")
local buttonGameObject = UIManager:Instantiate(buttonPrefab)
buttonGameObject:GetComponent(typeof(CS.UnityEngine.UI.Button)).onClick:AddListener(buttonListener)
end
function UIManager:Update()
-- print('ui_manager:Update')
end
-- 初始化预制体
function UIManager:Instantiate(prefab)
local gameObject = CS.UnityEngine.Object.Instantiate(prefab)
gameObject.transform:SetParent(CS.UnityEngine.GameObject.Find("Canvas").transform);
gameObject.transform.localRotation = CS.UnityEngine.Quaternion.identity;
gameObject.transform.localPosition = CS.UnityEngine.Vector3.zero;
gameObject.transform.localScale = CS.UnityEngine.Vector3.one;
gameObject.name = gameObject.name
return gameObject
end
-- button的监听
function buttonListener()
local vidioPlayerPrefab = ABManager:LoadAsset("prefabs","VideoPlayer")
local vidioGameObject = UIManager:Instantiate(vidioPlayerPrefab)
local rectTransform = vidioGameObject:GetComponent("RectTransform");
rectTransform.offsetMax = CS.UnityEngine.Vector2.zero;
rectTransform.offsetMin = CS.UnityEngine.Vector2.zero;
end
注意事项:
见https://blog.csdn.net/iiiiiiimp/article/details/128304154
注意事项:
(1)CubeManager脚本组件要实现特性[ExecuteInEditMode]
,这样其Update()
方法才能在鼠标在Scene中移动/点击时调用。
(2)CubeManagerEditor编辑器脚本用于编辑CubeManager脚本组件在Inspector面板中显示什么,所以要加入[CustomEditor(typeof(CubeManager))]
关联到CubeManager脚本组件,并在OnEnable()
方法中获取CubeManager脚本对象cubeManager = (CubeManager)target
。在OnInspectorGUI()
方法中描写要在CubeManager组件中绘制的按钮等,该方法只要每次CubeManager脚本组件值变化或则点击CubeManager脚本组件挂载的GameObject都会执行。
public override void OnInspectorGUI() {
Debug.Log("CubeManagerEditor:OnInspectorGUI");
//显示cubeList
serializedObject.Update();
SerializedProperty serializedProperty = serializedObject.FindProperty("cubes");
EditorGUILayout.PropertyField(serializedProperty, new GUIContent("节点"), true);
serializedObject.ApplyModifiedProperties();
//开始编辑按钮显示
if (isEditor==false && GUILayout.Button("开始连线")) {
Windows.OpenWindow(cubeManager.gameObject);
isEditor = true;
}
//关闭编辑
else if (isEditor && GUILayout.Button("结束连线"))
{
Windows.CloseWindow();
isEditor = false;
}
//删除最后一个节点
if (GUILayout.Button("删除最后一个连线"))
{
RemoveAtLast();
}//删除所以节点
else if (GUILayout.Button("删除所有连线"))
{
RemoveAll();
}
}
OnSceneGUI()
方法会在当鼠标在Scene视图下发生变化时执行,比如鼠标移动、点击。发射射线也在里面,因为Input.GetMouseButtonDown(0)
要在游戏运行时执行,而编辑器下游戏是没有运行的,所以鼠标监听用的是
Event.current.button == 0 && Event.current.type == EventType.MouseDown
同理从屏幕发射射线也不能用Camera.main.ScreenPointToRay(Input.mousePosition)
而是用
Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
代码
//当选中关联的脚本挂载的物体
//当鼠标在Scene视图下发生变化时,执行该方法,比如鼠标移动,比如鼠标的点击
private void OnSceneGUI() {
if (!isEditor)return;
//点击了鼠标左键
//非运行时,使用Event类 , 不能用Input.GetMouseButtonDown(0)
//Event.current.button 判断鼠标是哪个按键的
//Event.current.type 判断鼠标的事件方式的
if (Event.current.button == 0 && Event.current.type == EventType.MouseDown) {
RaycastHit hit;
//从鼠标的位置需要发射射线了
//因为是从Scene视图下发射射线,跟场景中的摄像机并没有关系,所以不能使用相机发射射线的方法
//从GUI中的一个点向世界定义一条射线, 参数一般都是鼠标的坐标
Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
if (Physics.Raycast(ray, out hit))
{
if (hit.transform.tag == "Plane") {//点到的是地板
GameObject prefab = Resources.Load<GameObject>("Prefabs/Cube");
GameObject cube = Instantiate(prefab, hit.point+ Vector3.up, prefab.transform.rotation);
cubeManager.cubes.Add(cube);
}else if (hit.transform.tag == "Cube") {//点到的是Cube
cubeManager.cubes.Add(hit.transform.gameObject); }
}
}
}
(3)之所以需要弹出一个窗口是因为当创建一个Cube过后,Unity会自动选中聚焦在这个Cube上,而在创建一个窗口并在Update()
中用Selection.activeGameObject = _plane
让Unity聚焦在挂有CubeManager脚本组件的Plane上。
private void Update() {
Debug.Log("Windows:Update");
//让选中焦点一直处于plan上,不是处于创建的cube上
if (Selection.activeGameObject!= null) {
Selection.activeGameObject = _plane;
}
}
注意事项:
左边是顶点片元着色器效果、右边是表面体着色器效果
(1)顶点片元着色器实现Phong光照
主纹理和法线纹理使用的是同一个float4 texcoodr:TEXCOORD0;
,这是因为主纹理和法线纹理的uv坐标是一样的
r.uvMainTexture = o.texcoodr.xy * _MainTexture_ST.xy + _MainTexture_ST.zw;
r.uvNormalTexture = o.texcoodr.xy * _BumpTexture_ST.xy + _BumpTexture_ST.zw;
使用float3 matrixRow1 : TEXCOORD4;float3 matrixRow2 : TEXCOORD5;float3 matrixRow3 : TEXCOORD6;
来存切线空间到世界空间的转换矩阵
float3 worldNormal = mul((float3x3)unity_ObjectToWorld,o.normal);
float3 worldTangent = mul((float3x3)unity_ObjectToWorld,o.tangent.xyz);
float3 worldBinormal = cross(worldNormal,worldTangent)*o.tangent.w;
r.matrixRow1 = float3(worldTangent.x,worldBinormal.x,worldNormal.x);
r.matrixRow2 = float3(worldTangent.y,worldBinormal.y,worldNormal.y);
r.matrixRow3 = float3(worldTangent.z,worldBinormal.z,worldNormal.z);
使用UnpackNormal()
解出法线纹理得到切线空间下的法线,然后点乘转换矩阵矩阵,将法线的切线空间转为世界空间下。
fixed4 bumpColor = tex2D(_BumpTexture,o.uvNormalT
fixed3 bump = UnpackNormal(bumpColor);
bump *= _BumpScale;
bump.z = sqrt(1 - max(0, dot(bump.xy, bump.xy)));
bump = fixed3(
dot(o.matrixRow1,bump),
dot(o.matrixRow2,bump),
dot(o.matrixRow3,bump)
);
漫反射和高光反射的法线就使用法线纹理的法线bump
//漫反射光照
fixed4 mainTextureColor = tex2D(_MainTexture,o.uvMainTexture)* _MainColor;
fixed3 diffuseColor = _LightColor0.rgb*mainTextureColor.rgb* (dot(normalize(bump),normalize(_WorldSpaceLightPos0.xyz))*0.5+0.5);
//高光反射
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz-o.worldPos.xyz);
fixed3 reflectDir = normalize(reflect(normalize(-_WorldSpaceLightPos0.xyz),normalize(bump)));
fixed3 specularColor = _LightColor0.rgb*_SpecularColor.rgb*pow(max(0,dot(viewDir,reflectDir)),_Gloss);
最后加上自发光
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.xyz * mainTextureColor.rgb + diffuseColor + specularColor;
return fixed4(color,1);
(2)表面体着色器实现Phong光照
表面体着色器比较简单
直接在pragma 里使用Lambert光照
#pragma surface surf Standard Lambert
把法线给Normal 把纹理给Albedo 即可。
struct Input
{
float2 uv_MainTex;
float2 uv_BumpTex;
};
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
half3 n = UnpackNormal(tex2D(_BumpTex,IN.uv_BumpTex));
o.Albedo = c.rgb;
o.Alpha = c.a;
o.Normal = n;
o.Smoothness = _Glossiness;
注意事项:
见https://blog.csdn.net/iiiiiiimp/article/details/127251001?
注意事项:
见https://blog.csdn.net/iiiiiiimp/article/details/127170580?
注意事项:
(1)数据库表的设计
商城表:存放物品和物品数量
装备信息表:存放装备的各个属性信息
人物信息表:存放人物的人物信息
人物装备表:存放人物的装备
(2)数据库的访问
更新用ExecuteNonQuery
返回单个结果用ExecuteScalar
返回多个结果用ExecuteReader
,这里我把ExecuteReader
结果用一个List
存起来方便之后调用,注意执行完ExecuteReader
一定要调用 _sqliteDataReader.Close();
public List<Dictionary<string, string>> ExecuteReaderSQL(string sql) {
List<Dictionary<string, string>> list = new List<Dictionary<string, string>>();
_sqliteCommand.CommandText = sql;
_sqliteDataReader = _sqliteCommand.ExecuteReader();
while (_sqliteDataReader.Read()) {//读一行
Dictionary<string, string> dictionary = new Dictionary<string, string>();
for (int i = 0; i < _sqliteDataReader.FieldCount; i++) {//读这一行得一列
Debug.Log(_sqliteDataReader.GetName(i)+":"+_sqliteDataReader.GetValue(i).ToString());
dictionary.Add(_sqliteDataReader.GetName(i),_sqliteDataReader.GetValue(i).ToString());
}
list.Add(dictionary);
}
//关闭读取器
_sqliteDataReader.Close();
return list;
}
(3)资源的访问
Assets/Plugins路劲下用于专门存放从外部导入的动态链接库,把sqlite3放在里面
Assets/Resources文件下用于存放图片预制体这些东西,这样就可以通过Resources.Load<>
访问,如拿到预制体
private void GetGameObject() {
bagEquipPrefab = Resources.Load<GameObject>("Prefabs/BagEquip");
shopEquipPrefab = Resources.Load<GameObject>("Prefabs/ShopEquip");
}
数据库放在Assets/StreamingAssets文件下然后通过Application.streamingAssetsPath
访问
string dataPath = "Data Source = " + Application.streamingAssetsPath + "/" + "UnitySQLite.db";
(4)装备的放置
使用Instantiate
直接将装备创建在对应Box的子物体上
GameObject bagGameObject = Instantiate(shopEquipPrefab, shopWindowTransform.GetChild(shopEquipCount));
(5)装备的监听
使用 _button.onClick.AddListener(方法名);
实现,这样就不用拖拽了
注意事项:
1、怪物的生成
核心思想就是用一个类MonsterWaveMessage
去存储每波怪物的信息,将这个类的对象放在一个数组里MonsterWaveMessage[]
,最后用遍历这个数组生成怪物即可。[System.Serializable]
是让Inspector面板能显示这个类。
[System.Serializable]
public class MonsterWaveMessage {
[Header("每波的时间间隔")]
public float waveInterval = 1f;
[Header("当前波怪物生成时间间隔")]
public float monsterCreateInterval = 1f;
[Header("当前波怪物数量")]
public int monsterCount = 3;
[Header("当前波怪物预设体")]
public GameObject monsterPrefab;
[Header("当前波怪物血量倍率")]
public int monsterHPRate = 1;
[Header("当前波怪物移动速度倍率")]
public int monsterSpeedRate = 1;
}
2、炮塔的射程
用的BoxCollider做的,调用OnTriggerEnter
当怪物进入到就把怪物加入的一个List中,默认攻击第一个。怪物离开或死亡时时用OnTriggerExit
移除List。因此怪物也要用一个List存放炮塔,好在自身死亡时通知炮塔将它移除。
进入射程
private void OnTriggerEnter(Collider other) {
if (other.gameObject.tag == "Monster") {
MonsterController monster = other.GetComponent<MonsterController>();
if (!_monsterList.Contains(monster)) {
//添加攻击目标
_monsterList.Add(monster);
//Monster添加攻击炮塔
monster.AddTowerController(this);
}
}
}
离开射程
private void OnTriggerExit(Collider other) {
if (other.gameObject.tag == "Monster") {
MonsterController monster = other.GetComponent<MonsterController>();
if (_monsterList.Contains(monster)) {
_monsterList.Remove(monster);
//移除被锁定的炮塔
monster.RemoveTowerController(this);
}
}
}
3、转向怪物,才能开炮
private float turn2Moster(MonsterController monster) {
Vector3 i2monster = monster.transform.position-turretTransform.position + Vector3.up * 1f + Vector3.forward * 0.5f;
Quaternion targetRoate = Quaternion.LookRotation(i2monster);
turretTransform.rotation = Quaternion.Lerp(turretTransform.rotation,targetRoate,turnSmoothSpeed * Time.deltaTime);
return Vector3.Angle(turretTransform.forward, i2monster);
}
4、怪物被攻击和死亡
由炮塔创建的炮弹去判断与怪物的距离,如果小于0.5米就判断为击中,就销毁跟随脚本,让炮火留在原地。并且通知击中的怪物减少血量,并且播放受伤动画或死亡动画
要注意的是怪物死亡的时候需要关闭刚体,导航,碰撞体,不能只关闭碰撞体,因为导航系统也有碰撞体,不关闭会让后面的怪物以为是障碍物。
private void Die() {
//关闭导航碰撞和碰撞体和刚体
Destroy(_rigidbody);
_navMeshAgent.isStopped = true;
_navMeshAgent.enabled = false;
_capsuleCollider.enabled = false;
this.liveState = LiveState.Die;
//通知Tower将自己移除
TowerRemoveMe();
_towerControllerList.Clear();
}
5、炮塔的生成
将所有炮塔设置为Tower一层,让后使用鼠标发射射线,layerMask只检测Tower这一层。
if (Input.GetMouseButtonDown(0)) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
LayerMask layerMask = LayerMask.GetMask("Tower");
//只检测Tower层的射线
if (Physics.Raycast(ray,out _raycastHit,100,layerMask) && currentChioceTowrtID != -1) {
Transform tower = _raycastHit.transform;
if (tower.childCount == 0) {//还没有放置炮塔
GameObject tow = Instantiate(towerPrefab[currentChioceTowrtID], Vector3.zero, Quaternion.identity);
tow.transform.parent = tower.transform;
tow.transform.localPosition = Vector3.up * 2.7f;
}
}
}
6、镜头的移动
就是用相机目前移动的位置加上当前鼠标移动的方向,然后用Mathf.Clamp
限定镜头在一定范围内。
if (Input.GetMouseButton(0))
{
Vector3 transPosition = cameraTrans.position
- Vector3.right * Input.GetAxisRaw("Mouse X") * sensitivityDrag * Time.timeScale
- Vector3.forward * Input.GetAxisRaw("Mouse Y") * sensitivityDrag * Time.timeScale;
cameraTrans.position = transPosition;
cameraTrans.position = new Vector3(
Mathf.Clamp(cameraTrans.position.x,20,40),
cameraTrans.position.y,
Mathf.Clamp(cameraTrans.position.z,2, 30));
}
if (i==0 || j == 0) {
G = 10;
} else {
G = 14;
}
G += centerCube.G;
H为当前点距离终点的估量代价,(为终点坐标-当前点坐标) * 10
H = (cubeEnd.X - currentCube.X + cubeEnd.Z - currentCube.Z) * 10;
F为F=G+H
3、中心点是用来确定下一步前进路线的,因为中心点的选取与H有关。而发现者finder是用来确定标记回去的路线的,因为发现者finder的选取与G有关且选的就是当前的中心点。
注意事项:
1、从站立到跑起来了融合树,让动作过度更加自然
动画状态机如下,使用RunSpeed(float)>0.1参数控制角色是站立还是奔跑,是使用
2、呼喊动画放置在第二层级,使用Trigger控制呼喊播放。骨骼遮罩只选择手和脑袋即可。需要注意的是,当角色呼喊后,触发条件为空且HasExitTime要勾选(一般是不勾选的),不然无法回到Empty。
3、角色静步和呼喊都是设置的虚拟按键,通过Input.GetButton("Sneak")
和Input.GetButton("Shout")
判断是否按住虚拟键。
4、角色转身代码,即先获取角色移动的方向moveDir = new Vector3(horAxis, 0, virAxis);
再把这个方向转为四元数moveQua = Quaternion.LookRotation(moveDir);
最后让角色准到这个方向即可transform.rotation = Quaternion.Lerp(transform.rotation, moveQua, Time.deltaTime * turnSpeed);
virAxis = Input.GetAxis("Vertical");
horAxis = Input.GetAxis("Horizontal");
runSpeedParameter = Animator.StringToHash("RunSpeed");
if (virAxis != 0 || horAxis!= 0) {
//播放动画
_animator.SetFloat(runSpeedParameter,MOVE_MAX_SPEED,0.3f,Time.deltaTime);
//获取移动方向
moveDir = new Vector3(horAxis, 0, virAxis);
//将方向转化为四元数
moveQua = Quaternion.LookRotation(moveDir);
//角色转身
transform.rotation = Quaternion.Lerp(transform.rotation, moveQua, Time.deltaTime * turnSpeed);
} else {
_animator.SetFloat(runSpeedParameter,0,0.1f,Time.deltaTime);
}
5、相机跟随代码
Vector3 followDir =cameraTransform.position - this.transform.position;
Vector3 moveDir = originalPlayer2Camera - followDir;
float moveSpeed = 3f;
cameraTransform.position =
Vector3.Lerp(cameraTransform.position, moveDir + cameraTransform.position, Time.deltaTime * moveSpeed);
注意事项:
1、装备拖动时的检测。为了检测装备是拖到哪里了,是装备栏吗?物品栏吗?等,就需要在拖动装备的时候检测鼠标移动的位置。因此在装备脚本中在OnBeginDrag
把装备的raycastTarget
属性关了,在OnEndDrag
中再开启,这样拖动装备时eventData.pointerEnter
得到的就是装备下面的UI控件了。因为如果不把raycastTarget
属性关了,则eventData.pointerEnter
一直检测的是拖动的装备UI。
public void OnBeginDrag(PointerEventData eventData) {
equipmentImageGetComponent.raycastTarget = false;
}
public void OnDrag(PointerEventData eventData) {
this.transform.position = Input.mousePosition;
Debug.Log(eventData.pointerEnter);
}
public void OnEndDrag(PointerEventData eventData) {
equipmentImageGetComponent.raycastTarget = true;
}
2、为了让拖动的物体显示在最上层,让其拖动时不被其他物体遮住,因此需要在拖动前OnBeginDrag
重新设置一下他的父物体为画布,并且记录一下拖动前的父物体
public void OnBeginDrag(PointerEventData eventData) {
//关闭射线
equipmentImageGetComponent.raycastTarget = false;
//拖动前的位置
beginDragParentTransform = this.transform.parent;
//更改变父对象,让其能显示在最上层
this.transform.SetParent(canvasTransform);
}
3、拖动结束后在OnEndDrag
中通过eventData
的tag
去判断是不是拖到了格子上或则装备上,不是的话返回原来的位置
public void OnEndDrag(PointerEventData eventData) {
GameObject eventDataGameObject = eventData.pointerEnter;
//放入空的装备栏 或则 空的背包栏 或则 已经装备了的背包栏或装备栏
if ((eventDataGameObject.tag == "EquipBox"||
eventDataGameObject.tag == "BagBox"||
eventDataGameObject.tag == "Equipment") &&
eventDataGameObject.transform != beginDragParentTransform
) {
if (eventDataGameObject.tag == "Equipment") {//已经装备了的背包栏或装备栏
eventDataGameObject.GetComponent<EquipmentController>().ReceiveEquipment(this.gameObject);
} else {//空的装备栏 或则 空的背包栏
eventDataGameObject.GetComponent<BaseBox>().ReceiveEquipment(this.gameObject);
}
} else {
BackToOriginalPosition();
}
equipmentImageGetComponent.raycastTarget = true;
}
4、格子接受装备很简单,直接把装备设为其子物体就行,再让其localPosition
归零。
public override void ReceiveEquipment(GameObject equipment) {
// Debug.Log(this);
equipment.transform.SetParent(this.transform);
equipment.transform.localPosition = Vector3.zero;
equipment.GetComponent<EquipmentController>().equipmentState = BaseEquipment.EquipmentState.BagBoxing;
}
注意事项:
1、关卡的摆放使用的是网格布局,先创建一个空物体,加上网格布局组件。然后将各个管卡设置为其子物体。各个关卡的初始化是使用代码初始化的,用GetChild
函数隐藏或显示UI。
2、关卡边缘红色的选择框移动使用的是selectFrame.SetParent(this.transform,false);
方法,false表示不会改变selectFrame的Transform组件的属性值。
3、界面的跳转用的是SceneManager.LoadScene(sceneName);
跳转后需要保存的数据用了单例模式去保存
public class SceneDataManager {
//单例
private static SceneDataManager ins;
//传输数据
private Dictionary<string, object> sceneOneshotData = null;
//管理星星的数量
private Dictionary<int, int> starDic;
public Dictionary<int, int> StarDic {
get { return starDic; }
}
public SceneDataManager() {
starDic = new Dictionary<int, int>();
}
public static SceneDataManager GetInstance() {
if (ins == null) {
ins = new SceneDataManager();
}
return ins;
}
}
4、找物体一般用FindWithTag、GetChild、Find
这些函数
5、关卡UI界面
层级要分明,想用一个创建一个空对象,把空对象的锚点设置在画面中心,然后空对象里面放UI
注意层级面板越在上面UI层级越低,就会被遮挡。
注意事项:
见https://blog.csdn.net/iiiiiiimp/article/details/125588752
注意事项:
1、用cameraTransform = Camera.main.transform
获取摄像机的位置
2、用Camera.main.ScreenPointToRay(Input.mousePosition)
将鼠标坐标转化为射线
3、用Physics.Raycast(mouseRay,out hit,rayDistance)
获取射线的碰撞体,然后用碰撞体位置hit.point
减去摄像机位置cameraTransform.position
就能得到小球发射的方法,再用Rigidbody.velocity
给这个方向一个速度即可
注意事项:
1、金币碰撞器用的网格碰撞器并且开启触发器,刚体组件开启重力。在OnTriggerEnter
方法中通过触发者的名字来判断是否与玩家(蓝色平板)发生处罚。
2、创建金币用的5个空对象CoinCreater1-5设置为一个空对象CoinCreaters的子对象,这样就能通过CoinCreaters脚本中的this.transform.GetChild(i)
获取子物体了。注意不要用this.gameObject.GetComponentsInChildren
,因为这个函数连父物体CoinCreaters也会获取到。
注意事项:
1、球体后面的白色伪影
2、为了让行星围绕恒星(中间红色的球)转,用了Vector3.Cross
叉乘求法向量,再用this.transform.RotateAround
函数。
注意事项:
1、选中摄像头,按Ctrl+Shift+F可以将摄像头快速移动到Scene画面的位置。
2、一般如果要创建一个复合物体(父子物体),那么最好用一个空物体作为根物体,把这个复合物体设为空物体的子物体,这样的好处就是整个物体的中心点就是空物体的中心点,并且避免了子物体拉伸旋转时出现变形。
例如一个椅子就可以分为腿、椅背、椅垫。
3、墙的透明材质
4、选中物体按W变为移位模式再按V可以对其进行贴合。
每个人的时间区间都不一样吧,不用太在意别人的眼光,趁着年轻还可以一无所有还可以重头再来时,多做自己想做的事情吧。