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

写在前面

个人兴趣+项目需要,学习一下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);
    }
    //自定义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<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得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

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");
    //显示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;
    }
}

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可以对其进行贴合。

滚球游戏(输入输出轴)

在这里插入图片描述

写在后面

每个人的时间区间都不一样吧,不用太在意别人的眼光,趁着年轻还可以一无所有还可以重头再来时,多做自己想做的事情吧。

  • 18
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值