本文主要是介绍Unity项目demo总结(已完成22项,持续更新ing,含商城、塔防、背包、动画、坦克大战等),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Unity项目demo总结
- 写在前面
- 烟花(粒子系统)
- 热更新(XLuaHotFix)
- 绘画涂鸦(图像处理、射线检测)
- Unity常用框架(对象池框架、状态机框架、UI框架)
- 视频播放(Lua调UnityAPI)
- AB包使用(异步加载AB包)
- 编辑器模式运行(Editor编辑器开发)
- Phong光照模型(顶点片元Shader、表面体Shader)
- 人物发光特效(表面体Shader)
- 图像渐变(固定管线Shader)
- 商城系统(SQLite访问)
- 3D塔防(AI寻路)
- A*寻路算法
- 动画系统(新版动画Mecanim)
- 背包系统(物品拖拽)
- 关卡选择UI界面(UGUI)
- 3D坦克大战(物体系统)
- 打砖块(射线检测)
- 拾取金币(碰撞检测)
- 行星绕恒星转动
- 个人场景搭建
- 滚球游戏(输入输出轴)
- 写在后面
写在前面
个人兴趣+项目需要,学习一下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
热更新(XLuaHotFix)
注意事项:
强烈推荐这篇博客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
Unity常用框架(对象池框架、状态机框架、UI框架)
对象池框架
可以看到从空中生成小球时不会创建新的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<string, Func<bool>> canTransitionStateDic;
存储该状态能过渡到的状态名和状态条件。注意是这个状态能过渡到的状态的状态名和状态条件,而不是能过渡到该状态的条件。
在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<string, State> controlStatesDic;
来存储状态。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<UIModuleBase> _uiModuleStack
存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();}}}
视频播放(Lua调UnityAPI)
注意事项:
(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);}//自定义Loaderprivate 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;}}//调用Luapublic 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<LuaBootstrap>("BootStrap");
将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得Boosstrappublic 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)
endfunction 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.namereturn 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
AB包使用(异步加载AB包)
注意事项:
见https://blog.csdn.net/iiiiiiimp/article/details/128304154
编辑器模式运行(Editor编辑器开发)
注意事项:
(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");//显示cubeListserializedObject.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") {//点到的是CubecubeManager.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;}
}
Phong光照模型(顶点片元Shader、表面体Shader)
注意事项:
左边是顶点片元着色器效果、右边是表面体着色器效果
(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;
人物发光特效(表面体Shader)
注意事项:
见https://blog.csdn.net/iiiiiiimp/article/details/127251001?
图像渐变(固定管线Shader)
注意事项:
见https://blog.csdn.net/iiiiiiimp/article/details/127170580?
商城系统(SQLite访问)
注意事项:
(1)数据库表的设计
商城表:存放物品和物品数量
装备信息表:存放装备的各个属性信息
人物信息表:存放人物的人物信息
人物装备表:存放人物的装备
(2)数据库的访问
更新用ExecuteNonQuery
返回单个结果用ExecuteScalar
返回多个结果用ExecuteReader
,这里我把ExecuteReader
结果用一个List<Dictionary<string, string>>
存起来方便之后调用,注意执行完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(方法名);
实现,这样就不用拖拽了
3D塔防(AI寻路)
注意事项:
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));
}
A*寻路算法
注意事项:
1、算法流程
2、G为当前点距离起点的估量代价
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有关且选的就是当前的中心点。
动画系统(新版动画Mecanim)
注意事项:
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;}
关卡选择UI界面(UGUI)
注意事项:
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层级越低,就会被遮挡。
3D坦克大战(物体系统)
注意事项:
见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<Transform>()
,因为这个函数连父物体CoinCreaters也会获取到。
行星绕恒星转动
注意事项:
1、球体后面的白色伪影
2、为了让行星围绕恒星(中间红色的球)转,用了Vector3.Cross
叉乘求法向量,再用this.transform.RotateAround
函数。
个人场景搭建
注意事项:
1、选中摄像头,按Ctrl+Shift+F可以将摄像头快速移动到Scene画面的位置。
2、一般如果要创建一个复合物体(父子物体),那么最好用一个空物体作为根物体,把这个复合物体设为空物体的子物体,这样的好处就是整个物体的中心点就是空物体的中心点,并且避免了子物体拉伸旋转时出现变形。
例如一个椅子就可以分为腿、椅背、椅垫。
3、墙的透明材质
4、选中物体按W变为移位模式再按V可以对其进行贴合。
滚球游戏(输入输出轴)
写在后面
每个人的时间区间都不一样吧,不用太在意别人的眼光,趁着年轻还可以一无所有还可以重头再来时,多做自己想做的事情吧。

这篇关于Unity项目demo总结(已完成22项,持续更新ing,含商城、塔防、背包、动画、坦克大战等)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!