【Unity】Mod形式的Dll及AssetBundle外部加载插件

2023-10-26

综述

本插件利用Mono.cecil静态注入模块(BepInEx包含的一个dll)实现在Unity游戏预加载(PreLoader)阶段的Dll修补工作,用以达到通过同版本Unity创建AssetBundle时候,无法打包脚本导致的游戏运行过程中利用Harmony等动态注入模块通过Hook函数或其他方式加载外部AssetBundle中的GameObject出现如下图所示的脚本缺失问题(The referenced script on this Behaviour is missing!)。
The referenced script on this Behaviour is missing!

使用方法

Github源码连接:点击此处查看

目录结构

只给出了与项目中所给例子相匹配的目录结构,具体结构自行结合实际修改。

  • BepInEx
    • config

    • core

    • patches

      • PatchMod.dll
      • PatchModInfo.dll
      • YamlDotNet.dll
    • plugins

      • RankPanel_Trigger.dll
      • BundleLoader
        • BundleLoader.dll
        • PatchModInfo.dll
        • YamlDotNet.dll
  • doorstop_config.ini
  • winhttp.dll
  • PatchMod
    • PatchMod.cfg
    • RankPanel
      • mods.yml
      • Dlls
        • Assembly-CSharp.dll
      • AseetBundles
        • rankpanel.ab
  • 其他文件

构建

PatchMod.dll 放入 BepInEx\patchers 文件夹中,将 BundleLoader.dll 放入 BepInEx\plugins 文件夹中。

对应Mod包的结构参考 PatchMod_Example.zip进行开发,将解压后的 PatchMod文件夹放入游戏根目录中。

PatchMod放置位置

目录中包含 PatchMod.cfg与各个Mod的包文件。

PatchMod.cfg文件内容如下:

[General]
# 是否预先加载进内存,预先加载进去可以防止其他Assembly-csharp加载
preLoad=true
# 是否将修补后的Dll输出到本地,用于调试查看
save2local=false

样板Mod中包含一个排行榜Mod,其打包过程如下:自己根据所要开发插件的游戏的Unity版本,用相同版本开发出组件并编写脚本,将要加入到游戏内的 Object打包为 AssetBundle,并记住其名字,然后插件项目整体进行构建,得到插件项目的 Assembly-csharp.dll,放到文件夹内。

Mod文件夹结构

在这里我的Dll文件放到了 Dlls文件夹下,AssetBundle文件放到了 Resources文件夹下,并在 mod.yml(拓展名为 .yml的文件即可)内编辑Mod相关设置。

# Mod名
name: 排行榜面板
# Dll读取路径
dlls:
    - Dlls/Assembly-CSharp.dll
# AssetBundle读取路径
resources:
    - AseetBundles/rankpanel.ab

进入游戏后在BepInEx控制台内即可以看到相关插件输出内容以及Mod组件加载列表。此后再根据其他插件Hook某些触发调用 Object即可。本项目内自带一个测试本用例的插件,亦可以下载完整版测试用例【金庸群侠传X】进行测试。

具体实现

首先是了解BepInEx插件,这是一个用于Unity/XNA游戏的外挂程序。
我们此次主要涉及三个部分:

  • 预加载时的Dll修补(Mono.cecil实现)
  • 游戏加载完成后的Bundle读取管理(Unity自带的Bundle管理机制)
  • 游戏内触发(Harmony2的动态修补Dll)

预加载修补

此部分具体参考的是IL-Repack项目,这是一个利用C#反射机制来进行Dll合并的项目(前身是IL-Merge,现已启用),IL-Repack的修补工作是通过魔改的一个Mono.cecil实现,本项目采用原版Mono.cecil模仿其实现。

using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PatchMod
{
    internal class MergeDll
    {
        //修复总函数
        internal static void Fix(string repairDllPath,ref AssemblyDefinition patchAssembly)
        {
            //修复用的文件(包含添加进去的内容)
            AssemblyDefinition repairAssembly = AssemblyDefinition.ReadAssembly(repairDllPath);
            //TODO:下面所有方法只修补二者MainModule.
            MergeDll.FixModuleReference(patchAssembly.MainModule, repairAssembly.MainModule);//修复引用
            foreach (TypeDefinition typeDef in repairAssembly.MainModule.Types)
            {
                //修复类型
                MergeDll.FixType(patchAssembly.MainModule, typeDef, (module, belongTypeDef, fieldDef) =>
                {
                    MergeDll.FixField(module, belongTypeDef, fieldDef);
                }, (module, belongTypeDef, methodDef) =>
                {
                    MergeDll.FixMethod(module, belongTypeDef, methodDef);
                });
            }
        }

        //修复Dll引用,将source添加到target中
        internal static void FixModuleReference(ModuleDefinition target, ModuleDefinition source)
        {
            foreach (ModuleReference modRef in source.ModuleReferences)
            {
                string name = modRef.Name;
                //如果存在重名则跳过修补
                if (!target.ModuleReferences.Any(y => y.Name == name))
                {
                    target.ModuleReferences.Add(modRef);
                }
            }
            foreach (AssemblyNameReference asmRef in source.AssemblyReferences)
            {
                string name = asmRef.FullName;
                //如果存在重名则跳过修补
                if (!target.AssemblyReferences.Any(y => y.FullName == name))
                {
                    target.AssemblyReferences.Add(asmRef);
                }
            }
        }

        //修复自定义类型,将source添加到target中
        //TODO:目前只能添加不同命名空间的类型
        internal static void FixType(ModuleDefinition target, TypeDefinition source, Action<ModuleDefinition, TypeDefinition, FieldDefinition> func_FixFeild, Action<ModuleDefinition, TypeDefinition, MethodDefinition> func_FixMethod)
        {
            //不合并同名Type
            //TODO:是否添加合并同名Type判断?
            if (!target.Types.Any(x => x.Name == source.Name))
            {
                //新建Type
                //如果是自定义Type直接Add会导致报错,因为属于不同的模块,
                //TODO:暂时没用unity工程的Assembly-csharp测试,不知道直接添加可否成功?
                //只向模块添加类型
                TypeDefinition importTypeDefinition = new(source.Namespace, source.Name, source.Attributes) { };
                //修复基类引用关系
                //例如 Component : MonoBehaviour
                if (source.BaseType != null)
                {
                    importTypeDefinition.BaseType = source.BaseType;
                }
                target.Types.Add(importTypeDefinition);


                //添加类型下的字段
                foreach (FieldDefinition fieldDef in source.Fields)
                {
                    func_FixFeild.Invoke(target, importTypeDefinition, fieldDef);
                }

                //添加类型下的方法
                foreach (MethodDefinition methodDef in source.Methods)
                {
                    func_FixMethod.Invoke(target, importTypeDefinition, methodDef);
                }
            }
        }

        //修复类型中的Field
        internal static void FixField(ModuleDefinition target, TypeDefinition typeDef, FieldDefinition fieldDef)
        {
            FieldDefinition importFieldDef = new(fieldDef.Name, fieldDef.Attributes, target.ImportReference(fieldDef.FieldType, typeDef));
            typeDef.Fields.Add(importFieldDef);
            importFieldDef.Constant = fieldDef.HasConstant ? fieldDef.Constant : importFieldDef.Constant;
            importFieldDef.MarshalInfo = fieldDef.HasMarshalInfo ? fieldDef.MarshalInfo : importFieldDef.MarshalInfo;
            importFieldDef.InitialValue = (fieldDef.InitialValue != null && fieldDef.InitialValue.Length > 0) ? fieldDef.InitialValue : importFieldDef.InitialValue;
            importFieldDef.Offset = fieldDef.HasLayoutInfo ? fieldDef.Offset : importFieldDef.Offset;
#if DEBUG
            Log($"Add {importFieldDef.FullName} to {typeDef.FullName}");
#endif
        }

        //修复类型中的Method
        internal static void FixMethod(ModuleDefinition target, TypeDefinition typeDef, MethodDefinition methDef)
        {
            MethodDefinition importMethodDef = new(methDef.Name, methDef.Attributes, methDef.ReturnType);
            importMethodDef.ImplAttributes = methDef.ImplAttributes;
            typeDef.Methods.Add(importMethodDef);
#if DEBUG
            Log($"Add {importMethodDef.FullName} to {typeDef.FullName}");
#endif

            //复制参数
            foreach (ParameterDefinition gp in methDef.Parameters)
            {
                ParameterDefinition importPara = new(gp.Name, gp.Attributes, gp.ParameterType);
                importMethodDef.Parameters.Add(importPara);
#if DEBUG
                Log($"Add Parameter {importPara.Name} to {importMethodDef.FullName}");
#endif
            }

            //修复Method函数体
            ILProcessor ilEditor = importMethodDef.Body.GetILProcessor();
            if (methDef.HasBody)
            {
                importMethodDef.Body = new Mono.Cecil.Cil.MethodBody(importMethodDef);
            }

            //TODO:没看懂,照搬
            if (methDef.HasPInvokeInfo)
            {
                if (methDef.PInvokeInfo == null)
                {
                    // Even if this was allowed, I'm not sure it'd work out
                    //nm.RVA = meth.RVA;
                }
                else
                {
                    importMethodDef.PInvokeInfo = new PInvokeInfo(methDef.PInvokeInfo.Attributes, methDef.PInvokeInfo.EntryPoint, methDef.PInvokeInfo.Module);
                }
            }

            //函数体参数
            foreach (VariableDefinition var in methDef.Body.Variables)
            {
                importMethodDef.Body.Variables.Add(new VariableDefinition(target.ImportReference(var.VariableType, importMethodDef)));
            }
            importMethodDef.Body.MaxStackSize = methDef.Body.MaxStackSize;
            importMethodDef.Body.InitLocals = methDef.Body.InitLocals;
            importMethodDef.Body.LocalVarToken = methDef.Body.LocalVarToken;

            //修复函数覆写
            foreach (MethodReference ov in methDef.Overrides)
                importMethodDef.Overrides.Add(target.ImportReference(ov, importMethodDef));

            //修改函数返回
            importMethodDef.ReturnType = target.ImportReference(methDef.ReturnType, importMethodDef);
            importMethodDef.MethodReturnType.Attributes = methDef.MethodReturnType.Attributes;
            importMethodDef.MethodReturnType.Constant = methDef.MethodReturnType.HasConstant ? methDef.MethodReturnType.Constant : importMethodDef.MethodReturnType.Constant;
            importMethodDef.MethodReturnType.MarshalInfo = methDef.MethodReturnType.HasMarshalInfo ? methDef.MethodReturnType.MarshalInfo : importMethodDef.MethodReturnType.MarshalInfo;

            //TODO:CustomAttribute还就那个不会
            foreach (var il in methDef.Body.Instructions)
            {
#if DEBUG
                Log($"Add IL {il.OpCode.OperandType} - {il.ToString()}");
#endif
                Instruction insertIL;

                if (il.OpCode.Code == Code.Calli)
                {
                    var callSite = (CallSite)il.Operand;
                    CallSite ncs = new(target.ImportReference(callSite.ReturnType, importMethodDef))
                    {
                        HasThis = callSite.HasThis,
                        ExplicitThis = callSite.ExplicitThis,
                        CallingConvention = callSite.CallingConvention
                    };
                    foreach (ParameterDefinition param in callSite.Parameters)
                    {
                        ParameterDefinition pd = new(param.Name, param.Attributes, target.ImportReference(param.ParameterType, importMethodDef));
                        if (param.HasConstant)
                            pd.Constant = param.Constant;
                        if (param.HasMarshalInfo)
                            pd.MarshalInfo = param.MarshalInfo;
                        ncs.Parameters.Add(pd);
                    }
                    insertIL = Instruction.Create(il.OpCode, ncs);
                }
                else switch (il.OpCode.OperandType)
                    {
                        case OperandType.InlineArg:
                        case OperandType.ShortInlineArg:
                            if (il.Operand == methDef.Body.ThisParameter)
                            {
                                insertIL = Instruction.Create(il.OpCode, importMethodDef.Body.ThisParameter);
                            }
                            else
                            {
                                int param = methDef.Body.Method.Parameters.IndexOf((ParameterDefinition)il.Operand);
                                insertIL = Instruction.Create(il.OpCode, importMethodDef.Parameters[param]);
                            }
                            break;
                        case OperandType.InlineVar:
                        case OperandType.ShortInlineVar:
                            int var = methDef.Body.Variables.IndexOf((VariableDefinition)il.Operand);
                            insertIL = Instruction.Create(il.OpCode, importMethodDef.Body.Variables[var]);
                            break;
                        case OperandType.InlineField:
                            insertIL = Instruction.Create(il.OpCode, target.ImportReference((FieldReference)il.Operand, importMethodDef));
                            break;
                        case OperandType.InlineMethod:
                            insertIL = Instruction.Create(il.OpCode, target.ImportReference((MethodReference)il.Operand, importMethodDef));
                            //FixAspNetOffset(nb.Instructions, (MethodReference)il.Operand, parent);
                            break;
                        case OperandType.InlineType:
                            insertIL = Instruction.Create(il.OpCode, target.ImportReference((TypeReference)il.Operand, importMethodDef));
                            break;
                        case OperandType.InlineTok:
                            if (il.Operand is TypeReference reference)
                                insertIL = Instruction.Create(il.OpCode, target.ImportReference(reference, importMethodDef));
                            else if (il.Operand is FieldReference reference1)
                                insertIL = Instruction.Create(il.OpCode, target.ImportReference(reference1, importMethodDef));
                            else if (il.Operand is MethodReference reference2)
                                insertIL = Instruction.Create(il.OpCode, target.ImportReference(reference2, importMethodDef));
                            else
                                throw new InvalidOperationException();
                            break;
                        case OperandType.ShortInlineBrTarget:
                        case OperandType.InlineBrTarget:
                            insertIL = Instruction.Create(il.OpCode, (Instruction)il.Operand);
                            break;
                        case OperandType.InlineSwitch:
                            insertIL = Instruction.Create(il.OpCode, (Instruction[])il.Operand);
                            break;
                        case OperandType.InlineR:
                            insertIL = Instruction.Create(il.OpCode, (double)il.Operand);
                            break;
                        case OperandType.ShortInlineR:
                            insertIL = Instruction.Create(il.OpCode, (float)il.Operand);
                            break;
                        case OperandType.InlineNone:
                            insertIL = Instruction.Create(il.OpCode);
                            break;
                        case OperandType.InlineString:
                            insertIL = Instruction.Create(il.OpCode, (string)il.Operand);
                            break;
                        case OperandType.ShortInlineI:
                            if (il.OpCode == OpCodes.Ldc_I4_S)
                                insertIL = Instruction.Create(il.OpCode, (sbyte)il.Operand);
                            else
                                insertIL = Instruction.Create(il.OpCode, (byte)il.Operand);
                            break;
                        case OperandType.InlineI8:
                            insertIL = Instruction.Create(il.OpCode, (long)il.Operand);
                            break;
                        case OperandType.InlineI:
                            insertIL = Instruction.Create(il.OpCode, (int)il.Operand);
                            break;
                        default:
                            throw new InvalidOperationException();
                    }
                //ilEditor.InsertAfter(importMethodDef.Body.Instructions.Last(),ilEditor.Create(OpCodes.Nop));
                importMethodDef.Body.Instructions.Add(insertIL);
#if DEBUG
                Log($"Add IL {il.OpCode.OperandType} - {insertIL.ToString()}");
#endif

            }
            importMethodDef.IsAddOn = methDef.IsAddOn;
            importMethodDef.IsRemoveOn = methDef.IsRemoveOn;
            importMethodDef.IsGetter = methDef.IsGetter;
            importMethodDef.IsSetter = methDef.IsSetter;
            importMethodDef.CallingConvention = methDef.CallingConvention;
        }
    }
}

具体调用过程请查看源码,此处只给出实现过程。首先我们合并引用和命名空间(此处只将外部不同命名空间合并而未做同名命名空间合并处理,因此简单不少),然后利用C#反射机制生成同样的类和方法、变量名,然后对于类的具体实现通过中间代码IL,同样借助反射复制过来(这一段直接抄IL-Repack的,因为自己不是很了解IL中间代码)。

Bundle读取管理

首先我们应该要做的是找一个和我们需要修改的游戏同版本的Unity来进行Bundle的打包,此处给出一种打包的实现,更为完善的还请百度一下。


    [MenuItem("AssetsBundle/Build AssetBundles")]
    static void BuildAllAssetBundles()//进行打包
    {
    	// Bundle输出目录
        string dir = "../AssetBundles_Generate";
        //判断该目录是否存在
        if (Directory.Exists(dir) == false) Directory.CreateDirectory(dir);//在工程下创建AssetBundles目录
        //参数一为打包到哪个路径,参数二压缩选项  参数三 平台的目标
        BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.UncompressedAssetBundle, BuildTarget.StandaloneWindows);
    }

生成Bundle之后,我们同样需要读取它,读取AssetBundle的方式很多,因为此处是本地加载,所以我没有写异步加载处理(也许会存在一些问题),具体实现如下:

using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using PatchModInfo;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace BundleLoader
{
    [BepInPlugin("com.EasternDay.BundleLoader", "Mod的Bundle加载器示例", "0.0.1")]
    public class BundleLoader : BaseUnityPlugin
    {
        // 日志记录
        private readonly static new ManualLogSource Logger = new("BundleLoader");

        // MOD文件读取路径
        private static readonly string modIndexPath = Path.Combine(Paths.GameRootPath, "PatchMod");

        //插件配置
        private static readonly List<ModInfo> mods = new();                 //Mod目录
        public static readonly Dictionary<string, Object> objects = new(); //游戏物体列表

        private void Awake()
        {
            // BepInEx将自定义日志注册
            BepInEx.Logging.Logger.Sources.Add(Logger);
        }

        private void Start()
        {
            //读取mods
            foreach (string dir in Directory.GetDirectories(modIndexPath))
            {
                ModInfo curInfo = ModInfo.GetModInfo(dir);
                mods.Add(curInfo);
                foreach (string bundlePath in curInfo.Resources)
                {
                    // 提示:Bundle打包请使用同版本Unity进行
                    foreach (Object obj in AssetBundle.CreateFromFile(bundlePath).LoadAllAssets())
                    {
                        Logger.LogInfo($"加载MOD资源:{dir}-{obj.name}");
                        objects.Add(obj.name, obj);
                    }
                }
            }
        }
    }
}

此处引入了一个自己定义的ModInfo类,并在游戏开始时读取所有Bundle并存储到字典中按照物体名来索引,后期在通过其他方式加载调用即可。

游戏内触发

运行时修补是修改方法而不永久修补它们的过程。运行时修补发生在游戏运行时,并且在 .NET 上可以非常广泛地完成。
BepInEx 附带 HarmonyXMonoMod.RuntimeDetour 来执行运行时修补。我选择使用HarmonyX进行修补,这是Harmony的一个分支,有兴趣当然看看最新版是最好的。

using BepInEx;
using HarmonyLib;
using JX_Plugin;
using JyGame;
using UnityEngine;
using UnityEngine.UI;

namespace RankPanel_Trigger
{
    [HarmonyPatch(typeof(RoleStatePanelUI), "Refresh")]
    class RoleStatePanelUI_Refresh_Patch
    {
        public static bool Prefix(RoleStatePanelUI __instance)
        {
            if (!__instance.transform.FindChild("HardIcon").GetComponent<Button>())
            {
                __instance.transform.FindChild("HardIcon").gameObject.AddComponent<Button>();
            }
            __instance.transform.FindChild("HardIcon").GetComponent<Button>().onClick.RemoveAllListeners();
            __instance.transform.FindChild("HardIcon").GetComponent<Button>().onClick.AddListener(() =>
            {
                GameObject go = (GameObject)GameObject.Instantiate(BundleLoader.BundleLoader.objects["RankPanel"]); //生成面板
                go.transform.SetParent(GameObject.Find("MapRoot/Canvas").transform);
                go.name = "RankPanel";
                go.SetActive(true);
                go.GetComponent<RankPanel>().SetContent("排行榜", LuaManager.Call<string>("RankPanel_Content", new object[0]));
            });
            return true;
        }
    }
    [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
    [BepInDependency("com.EasternDay.BundleLoader")]
    public class Plugin : BaseUnityPlugin
    {
        private static Harmony harmony = new("JX_Decode_Patch");
        private void Awake()
        {
            //Hook所有代码
            harmony.PatchAll();
            // 控制台提示语
            Logger.LogInfo($"插件 {PluginInfo.PLUGIN_GUID} 成功Hook代码!");
        }
    }
}

这里使用的是自动补丁,自动PatchAll(),然后我们进行一个RoleStatePanelUI的前挂补丁(Prefix)修补,并在修补完成后继续执行原函数(Prefix函数返回一个bool值,如果返回true,则代表原函数可以继续执行,如果返回false,则代表拦截原函数)。此处只给出一个小例子,具体的更多操作可以查看官方文档或者在我开源的代码里的解密插件可以看到更多用法。

温馨提示

采用本插件开发时,请注意 Nuget的包和引用库版本务必和自己的 Unity版本相匹配.

参考项目

名称 大概参考文件
IL-Repack ILRepack/ILRepack.cs及其所关联的文件
dnSpy 反编译器,使用下面的dnlib完成IL合并,最终没有选用,但有一定的参考意义
dnlib 最终没有选用,但有一定的参考意义
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Unity】Mod形式的Dll及AssetBundle外部加载插件 的相关文章

  • UE4 解决景深效果闪烁问题

    原因 1 模型的垂直竖线 造成抗锯齿算法对竖线的渲染计算 处于一种不稳定的状态 因此闪烁 解决办法 使用LOD 用贴图去替代线条模型 2 材质的法线贴图 当法线贴图含有垂直竖线的纹理效果 也会造成闪烁 比如这种幕墙材质 解决办法 关闭或动态
  • Unity中级客户端开发工程师的进阶之路

    上期UWA技能成长系统之 Unity高级客户端开发工程师的进阶之路 得到了很多Unity开发者的肯定 通过系统的学习 可以掌握游戏性能瓶颈定位的方法和常见的CPU GPU 内存相关的性能优化方法 UWA技能成长系统是UWA根据学员的职业发展
  • FBX导入Unity中模型没有材质的处理

    一 3dMax导出FBX时的注意事项 导出时 确保maps文件存在 里面放着fbx用到的image 二 在Unity中的设置 1 文件拖入Unity的Assets文件夹中 2 查看模型的材质是否存在 如下所示 材质为None 此时拖入sce
  • Unity动画系统详解

    目录 动画编辑器 编辑器面板 动画复用 前言 人形重定向动画 Humanoid 通用动画 Generic 旧版本动画 Legacy 动画控制器 系统状态 切换条件 状态机脚本 IK动画 反向动力学 BlendTree 混合树 Animato
  • unity中创建询问弹出窗口

    在开发过程中进程会遇到需要弹出一个窗口询问用户是否进行的操作 今天就来制作一个这样弹出窗口 然后根据弹出窗口的选择内容不同进行不同的操作 本例中主要是为了删除一个数据 而在删除数据操作前需要得到用户的一个确认操作 这里面主要用到了Notif
  • unity后台加密时间锁

    前言 在做一些项目的时候 有些不良甲方在给完项目后会有不给尾款的情况 之前都是加一些水印啥的 感觉不是很方便 第一不美观 第二如果甲方给完尾款后还得重新打包去水印 然后又做过一个本地的时间锁 等到时间 程序直接退出 但是感觉还是不方便 有时
  • unity工程崩溃资源找回

    1 Unity死机未保存场景 当你在Unity中编辑场景 突然死机时 可以在项目文件目录中找到Temp文件夹 双击文件夹 找到 Backupscenes文件夹 把后缀为 backup的文件后缀改为 unity 然后拖进Unity的Proje
  • Unity保存图片到相册

    Unity保存图片到Android相册 Java 纯文本查看 复制代码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
  • unity dots jobSystem 记录

    Looking for a way to get started writing safe multithreaded code Learn the principles behind our Job System and how it w
  • unity3d切换场景Application.LoadLevel(1)含义

    Application LoadLevel 1 场景ID
  • unity3d image组件不显示

    需要将UI组件放到画布下面
  • 【Unity】如何让Unity程序一打开就运行命令行命令

    背景 Unity程序有时依赖于某些服务去实现一些功能 此时可能需要类似打开程序就自动运行Windows命令行命令的功能 方法 using UnityEngine using System Diagnostics using System T
  • Unity学习笔记

    一 旋转欧拉角 四元数 Vector3 rotate new Vector3 0 30 0 Quaternion quaternion Quaternion identity quaternion Quaternion Euler rota
  • Unity中URP下的指数雾

    文章目录 前言 一 指数雾 雾效因子 1 FOG EXP 2 FOG EXP2 二 MixFog 1 ComputeFogIntensity 雾效强度计算 2 lerp fogColor fragColor fogIntensity 雾效颜
  • U3D游戏开发中摇杆的制作(NGUI版)

    在PC端模拟摇杆 实现控制摇杆让玩家或者物体移动 以下是完整代码 using System Collections using System Collections Generic using UnityEngine public clas
  • 游戏开发常见操作梳理之NPC任务系统

    多数游戏存在任务系统 接下来介绍通过NPC触发任务的游戏制作代码 using System Collections using System Collections Generic using UnityEngine
  • 游戏开发常见操作梳理系列之——玩家信息的显示系统

    在游戏中 有不少游戏在左上角会出现玩家的头像和等级以及血量 这就是玩家的信息显示系统 那么这些是如何制作的呢 接下来我将讲讲代码的操作 其它操作我会在其它笔记中一一说明 敬请期待 信息的显示相当简单就是控制一些UI 然后在其它系统里面填写相
  • 游戏开发常见操作梳理之NPC药品商店系统(NGUI版)

    后续会出UGUI Json的版本 敬请期待 游戏开发中经常会出现药品商店 实际操作与武器商店类似 甚至根据实际情况可以简化设置 废话不多说 直接上代码 药品商店的源码 using System Collections using Syste
  • 游戏开发之常见操作梳理——武器装备商店系统(NGUI版)

    游戏开发中经常出现武器商店 接下来为你们带来武器装备商店系统的具体解决办法 后续出UGUI Json版本 敬请期待 武器道具的具体逻辑 using System Collections using System Collections Ge
  • 游戏开发中常见系统梳理之背包系统的实现一

    游戏中几乎都存在大大小小的背包系统 接下来我将讲述背包系统具体是如何实现的 完整源码 以下是使用unity NGUI实现 使用txt配置的方法 后续更新UGUI Json实现的背包系统敬请期待 背包中的物品我们常常将其制作成预设体 通过改变

随机推荐

  • Hive性能调优策略

    利用分区表优化 场景 在业务环境中 以某个字段为筛选条件的需求增加 解决方法 建立以这个字段为分区的分区表 这样进行查询时只需要指定这个分区就不再需要进行全表扫描 利用分桶表优化 场景 需要频繁进行采样 解决方法 分桶表会使用hash算法将
  • matlab 层次聚类

    MATLAB的统计工具箱中的多元统计分析中提供了聚类分析的两种方法 1 层次聚类 hierarchical clustering 2 k means聚类 这里用最简单的实例说明以下层次聚类原理和应用发法 层次聚类是基于距离的聚类方法 MAT
  • Mysql最大连接数,TimeOut配置

    Mysql连接数配置 1 MySQL的max connections参数用来设置最大连接 用户 数 每个连接MySQL的用户均算作一个连接 max connections的默认值为100左右 1 1查看数据库配置的最大连接数 show va
  • java核心技术卷 之单选按钮

    在前一个例子中 对于两个复选框 用户既可以选择一个 两个 也可以两个都不选 在很多情况下 我们需要用户只选择几个选项当中的 一个 与用户选择另一项的时候 前一项就自动地取消选择 这样一组选框通常称为单选按钮组 Radio Button Gr
  • 关于mapper接口注入spring容器

    mapper是接口 而接口时不能注入spring容器的 要注入就需要接口有对应的实现类 注入的应该是实现类而不是接口 而在spring中 导入MyBatis Spring包之后 MyBatis Spring中间件把mapper接口和mapp
  • 新文件创建inode分配路径

    0 ext4 new inode handle 0x0
  • java数组工具类(遍历(display)、添加(append)、删除(remove)、查找(indexOf) 、排序(sort))

    设计一个类 用于数组操作 1 成员变量为一个一维数组 数组元素是int型 2 构造方法中包含以一维数组为参数的构造方法 3 成员方法包括数组遍历 display 添加 append 删除 remove 查找 indexOf 排序 sort
  • Python分析成绩

    目录 一 准备工作 二 所用到的库 1 Numpy 2 Matplotlib 3 Pandas 三 代码实现 1 理科成绩分析 py 2 文理科成绩对比分析 py 四 效果展示 一 准备工作 1 某学校高三文科班一模学生成绩表 csv 2
  • jquery二维码生成插件jquery.qrcode.js

    http www jq22 com jquery info294 插件描述 jquery qrcode js 是一个能够在客户端生成矩阵二维码QRCode 的jquery插件 使用它可以很方便的在页面上生成二维条码 如何使用它 将jquer
  • IDEA之JDBC使用教程

    目录 1 下载JDBC 2 创建项目 3 导入驱动包 4 JDBC常用接口 5 JDBC使用步骤 6 JDBC demo样例 1 下载JDBC 下载地址 JDBC驱动包百度网盘下载地址 提取码 3l6c 2 创建项目 步骤如下 3 导入驱动
  • 零撸项目-Star Network注册流程

    大家好 我是面具少年 本次主要讲解 Star Network注册流程 Star Network 本项目仅零撸 不建议投资 内容仅供参考 具有支付功能的社交 DeFi 去中心化金融 网络 是未来的去中心化金融平台 具有交换借贷 钱包和支付功能
  • list_del使用错误,如果摘链后还有挂链,请使用list_del_init。否则引发血案!!!

    rt
  • 开发下载成套的icon图标的知识

    下载成体系的icon图标 再也不用到处找成套的图标了 iconfont 阿里巴巴矢量图标库 选择完之后选择点赞靠前的一个 进入之后 ctrl f 输入需要的图标 会自动检索当前页面的所有图标 然后快速定位 选择你喜欢的下载就好了 拜拜
  • Mac安装VM虚拟机

    一 所需文件 VMware Fusion Pro CentOS 7 x86 64 Minimal 2003 iso 二 下载 去vm官网下载vm安装包 下载完成后进行安装 三 安装 如下图所示 双击安装包 进行安装 把下载好的centos拖
  • 【单元测试】Google Test(GTest)和Google Mock(GMock)--编辑中

    目录 Gtest简介 局限性 入门例子 还可以打印信息 进阶 测试我们函数的API ASSERT 和EXPECT TEST TEST F TEST P的区别 ASSERT 和EXPECT 说明 简单的测试例子 Test Fixtures 为
  • 网页前端开发

    内容 智能表单样式扩展 max width 表示最大宽度 text align 字体居中 某些属性样式直接写到form里面不行 需要写在style 里面 CSS入门 CSS入门 CSS简介 CSS指的是Cascading Style She
  • MySQL主键约束(PRIMARY KEY ,PK)

    MySQL主键约束 PRIMARY KEY PK 在数据库中使用过程中 如果 想将某个字段作为唯一标识 标记所有内容时 则可以使用PK 约束进行设置 即PK约束在创建数据库表时为某些字段加上 PRIMARY KEY 约束条件 则该字段可以唯
  • 一张表看清哪些企业属于阿里大厂版图

    一张表看清哪些企业属于阿里大厂版图 百胜餐饮集团已经宣布与春华资本集团及蚂蚁金融服务集团达成协议 二者共同向百胜中国投资4 60亿美元 该项投资将与百胜餐饮集团与百胜中国的分拆同步进行 蚂蚁金服将帮助百胜中国为旗下品牌提供移动支付服务 包括
  • RuntimeError: cuda runtime error (11) : invalid argument at /pytorch/aten/src/THC/THCGeneral.cpp

    RuntimeError cuda runtime error 11 invalid argument at pytorch aten src THC THCGeneral cpp cuda9 0 torch0 4 解决办法 在demo p
  • 【Unity】Mod形式的Dll及AssetBundle外部加载插件

    综述 本插件利用Mono cecil静态注入模块 BepInEx包含的一个dll 实现在Unity游戏预加载 PreLoader 阶段的Dll修补工作 用以达到通过同版本Unity创建AssetBundle时候 无法打包脚本导致的游戏运行过