分兩部分回答,數據和邏輯。

首先是數據,我在之前的文章中使用的這些方法:

MaxwellGeng:基於Unity3D的大地形研究(1):Cluster Async Load?

zhuanlan.zhihu.com圖標MaxwellGeng:基於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& HiddenCanvases = new 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引擎製作的遊戲反推一下實現(比如《城市天際線》)。

  1. 弄清玩家如何製作mod。
  2. 玩家製作的內容相當於一份配置文件,接下來要做的就是如何根據這份配置生成遊戲。


推薦閱讀:
相关文章