如果我用Unity或者虛幻4開發一個遊戲,怎樣讓我的遊戲支持讓玩家自製mod修改遊戲這一功能?
分兩部分回答,數據和邏輯。
首先是數據,我在之前的文章中使用的這些方法:
MaxwellGeng:基於Unity3D的大地形研究(1):Cluster Async Load?zhuanlan.zhihu.comMaxwellGeng:基於Unity3D的大地形研究(2):資源序列化與材質載入?zhuanlan.zhihu.com
是使用二進位的方法,將場景渲染數據通過二進位儲存,並在實時環境下使用文件流進行讀取和非同步載入的方法。使用這種方法的好處在於:不受限於引擎本身的打包,可以做到將遊戲項目和遊戲文件獨立開來(B社:小夥子有前途),同時也不受限於Unity的場景載入邏輯,用戶完全可以通過輸入的數據自行開發一個32KM的大地形,然後在遊戲中依靠現有的渲染邏輯自動非同步載入卸載。這不僅限於渲染,場景的物理碰撞,邏輯腳本等當然都可以用這樣的方法進行序列化和反序列化。
這種序列化方法我現在可謂樂此不疲,包括我目前正在開發的給這套地形系統使用的GI,都在採用這種方法,如果要開發此類功能,只需要後期給用戶提供一個編輯器程序,用戶就可以自行開發了。
然後是邏輯,邏輯可以選擇提供腳本語言介面,比如Python或者Lua,腳本語言雖然性能不大行,但是上限也還是很高的,如果願意你把多線程和內存管理介面提供給用戶都行……(大霧)。
剩下的大概就是一些設計模式,框架方面的工作了,這個沒什麼捷徑可以走,只能應對不同項目類型提供不同方案然後慢慢試錯。
介紹下我遊戲的Mod的實現機制 基於 Unity + C# 反射 + 介面約束
測試過安卓平台與電腦端。
代碼如下:
//ScriptableModManager.cs
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.SceneManagement;namespace ShanghaiWindy.Core
{public class ScriptableModManager : MonoBehaviour
{
public List& dllDirs = new List&();private System.Action OnUpdated;
private System.Action OnFixedUpdate;
private System.Action OnUpdateGUI;
private System.Action& OnNewSceneLoaded;
public void Start()
{
DontDestroyOnLoad(gameObject);
Mount();
}public void Mount()
{
SceneManager.sceneLoaded += (scene, mod) =&> { OnNewSceneLoaded?.Invoke(scene.name); };foreach (var assemblyFile in dllDirs)
{
try
{
var assembly = Assembly.LoadFrom(assemblyFile);
Module[] modules = assembly.GetModules();foreach (Module module in modules)
{
var types = module.GetTypes();
foreach (var type in types)
{
//Mount General Interface
var generalInterface = type.GetInterface("IGeneralAddOn");if (generalInterface != null)
{
var instance = Activator.CreateInstance(type);var InitializedMethod = type.GetMethod("OnInitialized");
var OnUpdateMethod = type.GetMethod("OnUpdate");
var OnFixedUpdateMethod = type.GetMethod("OnFixedUpdate");
var OnUpdateGUIMethod = type.GetMethod("OnUpdateGUI");
var OnNewSceneLoadedMethod = type.GetMethod("OnNewSceneLoaded");try
{
InitializedMethod.Invoke(instance, null);
}
catch { }OnUpdated += () =&>
{
try
{
OnUpdateMethod.Invoke(instance, null);
}
catch { }
};OnFixedUpdate += () =&>
{
try
{
OnFixedUpdateMethod.Invoke(instance, null);
}
catch { }
};OnUpdateGUI += () =&>
{
try
{
OnUpdateGUIMethod.Invoke(instance, null);
}
catch { }
};
OnNewSceneLoaded += (sceneName) =&>
{
try
{
OnNewSceneLoadedMethod.Invoke(instance, new object[] { sceneName });
}
catch { }
};
}
}
}
}
catch (Exception exception)
{
Debug.Log(exception.Message);
}}
}private void Update()
{
OnUpdated?.Invoke();
}private void FixedUpdate()
{
OnFixedUpdate?.Invoke();
}private void OnGUI()
{
OnUpdateGUI?.Invoke();
}}
}//IGeneralAddOn.cs
namespace UnityMod
{
public interface IGeneralAddOn
{
/// &/// On AddOn is loaded void OnInitialized();
/// &
/// &/// On Unity Monobehaviour Call Update void OnUpdate();
/// &
/// &/// On Unity Monobehaviour Call FixedUpdate void OnFixedUpdate();
/// &
/// &/// On New Scene is loaded void OnNewSceneLoaded(string name);
/// &
/// &/// On Unity Monobehaviour Call OnGUI void OnUpdateGUI();
/// &
}
}
ScriptableModManager.cs 放在遊戲里執行,需要設置 dllDirs(載入的Mod的Dll的路徑)
功能主要就是,載入Dll,然後實例化繼承於某介面的對象,調用裡面的函數。
IGeneralAddOn.cs 單獨打包為一個Dll 扔給玩家即可
約束方法,保證ScriptableModManager的正確代碼調用。
自定義Mod 的編寫(隱藏遊戲界面的功能Mod)
using UnityEngine;
using UnityMod;namespace UnityModExample
{
public class HideGameUI :IGeneralAddOn
{
public bool toggleUI = true;
public List&public void OnInitialized()
{
}public void OnUpdate()
{
if (Input.GetKeyDown(KeyCode.V))
{
toggleUI = !toggleUI;
//Show Hidden UIs
if (toggleUI)
{
foreach (var canvas in HiddenCanvases)
{
if(canvas!=null)
canvas.gameObject.SetActive(toggleUI);
}
}
else
{
foreach (var canvas in GameObject.FindObjectsOfType&())
{
HiddenCanvases.Add(canvas);
canvas.gameObject.SetActive(toggleUI);
}
}}
}public void OnFixedUpdate()
{
}public void OnNewSceneLoaded(string name)
{}
public void OnUpdateGUI()
{
}
}
}
如果需要調用遊戲內的腳本,可以用Unity新出的 asmdef 定義遊戲邏輯為單獨一個Dll。然後玩家的Mod中可以單獨導入這個Dll,編寫強功能性Mod。
比如,如何給自己的遊戲寫一個作弊器Mod?
答:
using ShanghaiWindy.Core;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityMod;namespace UnityModExample
{
public class PlaygroundCheaterPlayerData : IVehicleAddOn
{
public static TankInitSystem playerVehicle;public void OnVehicleLoaded(int instanceID)
{
foreach (var vehicle in GameObject.FindObjectsOfType&())
{
if (vehicle.gameObject.GetInstanceID() == instanceID)
{
if (vehicle._InstanceNetType == InstanceNetType.GameNetWorkOffline)
{
playerVehicle = vehicle;var defaultReloadTime = playerVehicle.vehicleComponents.mainTankFire.tankFireParams.ReloadTime;
var defaultAdvanceReloadTime = playerVehicle.vehicleComponents.mainTankFire.tankFireParams.advanceFireClass.LargeReloadTime;//Fast Reload Binding
PlaygroundCheater.OnToggleFastReload = (state) =&>
{
if (state)
{
playerVehicle.vehicleComponents.mainTankFire.tankFireParams.ReloadTime = 0.5f;
playerVehicle.vehicleComponents.mainTankFire.tankFireParams.advanceFireClass.LargeReloadTime = 0.5f;
}
else
{
playerVehicle.vehicleComponents.mainTankFire.tankFireParams.ReloadTime = defaultReloadTime;
playerVehicle.vehicleComponents.mainTankFire.tankFireParams.advanceFireClass.LargeReloadTime = defaultAdvanceReloadTime;
}
};
}
}
}
}
}public class PlaygroundCheater : IGeneralAddOn
{
public static string currentScene;public static System.Action& OnToggleFastReload;
private bool isFastReload = false;
private Rect winRect = new Rect(45, 45, 400, 500);
private bool isAttackable = true;
private bool isFolder = false;
private List& vehicleSpawn = new List&();
private Vector2 scrollPosition;
public void OnFixedUpdate()
{}
public void OnInitialized()
{
}public void OnNewSceneLoaded(string name)
{
currentScene = name;vehicleSpawn = VehicleInfoManager.Instance.vehicleList;
}public void OnUpdate()
{}
public void OnUpdateGUI()
{
if (!currentScene.Contains("Training"))
{
return;
}if (PlaygroundCheaterPlayerData.playerVehicle == null)
{
return;
}GUILayout.Label("Playground Cheater Mod Active. Author:Doreamonsky");
winRect = GUI.Window(1, winRect, (winID) =&>
{
isFolder = GUILayout.Toggle(isFolder, "is Fold");if (isFolder)
{
winRect.size = new Vector2(400, 50);
GUI.DragWindow();return;
}
else
{
winRect.size = new Vector2(400, 500);
}GUILayout.Label("Property Modfication");
GUILayout.Label($"Fast Reload State:{isFastReload.ToString()}");
if (GUILayout.Button("Fast Reload Toggle"))
{
isFastReload = !isFastReload;OnToggleFastReload?.Invoke(isFastReload);
}GUILayout.Space(25);
GUILayout.Label("Spawn Manager");
isAttackable = GUILayout.Toggle(isAttackable, "is Attackable");
scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300));
foreach (var vehicle in vehicleSpawn)
{
if (GUILayout.Button($"Spawn: {vehicle.vehicleName}"))
{
var rv = Random.insideUnitCircle;var expectPos = new Vector3(rv.x, 0, rv.y) * Random.Range(10, 150);
var navHit = new NavMeshHit();
var isHit = NavMesh.SamplePosition(expectPos, out navHit, 500, 1 &() as BotLogic : ScriptableObject.CreateInstance&() as BotLogic, navHit.position, Vector3.zero);
}
}GUILayout.Space(5);
}GUILayout.EndScrollView();
GUI.DragWindow();
}, "Cheater");
}private void CreateBot(string _vehicle, TeamManager.Team _botTeam, BotLogic botLogic, Vector3 pos, Vector3 euler)
{
TankInitSystem vehicle = new GameObject("Vehicle", typeof(TankInitSystem)).GetComponent&();
vehicle.VehicleName = _vehicle;
vehicle._InstanceNetType = InstanceNetType.GameNetworkBotOffline;
vehicle.ownerTeam = _botTeam;vehicle.BulletCountList = new int[] { 1000, 1000, 1000 };
vehicle.thinkLogic = botLogic;vehicle.InitTankInitSystem();
vehicle.transform.position = pos;
vehicle.transform.eulerAngles = euler;
}
}
}
日後補充,如何製作Mod地圖與人物等。
沒有做過這樣的功能,虛幻4官方有篇文章介紹了怎麼讓你的項目支持mod功能,如果感興趣的話可以自己看一下:
Modding: Adding mod-support to your Unreal Engine 4 project?wiki.unrealengine.com難點主要在於外部模型、動畫等資源的導入,數據修改算是最容易的,邏輯修改則需要引入腳本語言。
如果是ue4,可以參照方舟、絲瓜、柯南...直接用ue4編輯器作為mod編輯器,玩家也可以直接在藍圖上寫邏輯改數值,不過會暴露大量的內容和實現。
另一條路就是支持外部fbx導入、解析,還需要實現簡易的材質編輯器或者寫死幾種貼圖位置,這也是市面上大多數遊戲mod的做法,不過要花費一番工夫了。
如果讓用戶自行修改動畫,就要將動畫系統高度模塊化,比如某個人物單手劍攻擊固定只有4種、持劍移動固定只有16種,用戶替換對應文件就行了。
至於數據就以json、xml之類的文件格式存放,用的時候從文本文件讀,用戶直接記事本打開就能自由修改判定幀、對話、屬性...
腳本有很多選擇,遊戲行業lua用得很廣泛,也有類python的(騎砍),總之要在遊戲原來的c++/c#代碼外再封裝一層。
綜上,商業引擎要實現mod功能還是有不小的工作量的了,當然如果是自研引擎這些工作量不算什麼,在決定支持mod前要權衡利弊,遊戲的賣點是什麼,玩家數量能否支持mod社區生態。
這倆引擎集成lua都很方便的,ue4直接寫C++,unity用c#,lua都支持。主流開放mod的遊戲很多都是用lua。這要求遊戲邏輯很多都要從lua輸入,不再能直接寫源碼,是個蠻繁瑣的工作,而且執行效率也會有所降低。但為了mod很多時候是值得的。
數據表也可以放出來,比如集成一個xml/json解析器,然後把配置文件搞成相應格式,平時用原始文件,mod載入時按名字改掉一部分原始數據即可。也可以用表驅,直接把配表扔給玩家即可。
以上兩種方式都是費程序員的時間和精力的,太懶的話,unity寫邏輯是C#,你甚至可以直接把遊戲部分邏輯dll扔出來,讓玩家自己改。現在各種C#反彙編工具已經相當成熟了,甚至不需要懂il,真的非常適合拿來熱更新,可以作為最終手段。
看到好多人說Lua方案麻煩,其實真不麻煩,隨手找個lua熱更新的開發框架(Github上一大堆)改改就完事了
實在不喜歡lua的話,找個ilruntime的框架改改也行
把遊戲代碼與配置腳本化並公開,譬如使用Lua。
Unity估計不好搞。
UE4源碼開放,改一改,也可以發布Mod編輯器
沒做過,不了解虛幻4,如果是Unity的話,感覺可以找個用Unity引擎製作的遊戲反推一下實現(比如《城市天際線》)。
- 弄清玩家如何製作mod。
- 玩家製作的內容相當於一份配置文件,接下來要做的就是如何根據這份配置生成遊戲。
推薦閱讀: