Unity项目demo总结(已完成22项,持续更新ing,含商城、塔防、背包、动画、坦克大战等)

本文主要是介绍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中通过eventDatatag去判断是不是拖到了格子上或则装备上,不是的话返回原来的位置

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,含商城、塔防、背包、动画、坦克大战等)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/396596

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

poj2576(二维背包)

题意:n个人分成两组,两组人数只差小于1 , 并且体重只差最小 对于人数要求恰好装满,对于体重要求尽量多,一开始没做出来,看了下解题,按照自己的感觉写,然后a了 状态转移方程:dp[i][j] = max(dp[i][j],dp[i-1][j-c[k]]+c[k]);其中i表示人数,j表示背包容量,k表示输入的体重的 代码如下: #include<iostream>#include<

hdu2159(二维背包)

这是我的第一道二维背包题,没想到自己一下子就A了,但是代码写的比较乱,下面的代码是我有重新修改的 状态转移:dp[i][j] = max(dp[i][j], dp[i-1][j-c[z]]+v[z]); 其中dp[i][j]表示,打了i个怪物,消耗j的耐力值,所得到的最大经验值 代码如下: #include<iostream>#include<algorithm>#include<

csu(背包的变形题)

题目链接 这是一道背包的变形题目。好题呀 题意:给n个怪物,m个人,每个人的魔法消耗和魔法伤害不同,求打死所有怪物所需的魔法 #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<queue>#include<set>//#include<u>#include<map

hdu1011(背包树形DP)

没有完全理解这题, m个人,攻打一个map,map的入口是1,在攻打某个结点之前要先攻打其他一个结点 dp[i][j]表示m个人攻打以第i个结点为根节点的子树得到的最优解 状态转移dp[i][ j ] = max(dp[i][j], dp[i][k]+dp[t][j-k]),其中t是i结点的子节点 代码如下: #include<iostream>#include<algorithm

poj3468(线段树成段更新模板题)

题意:包括两个操作:1、将[a.b]上的数字加上v;2、查询区间[a,b]上的和 下面的介绍是下解题思路: 首先介绍  lazy-tag思想:用一个变量记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。 比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,