MOD制作チュートリアル

modを作る、それは世界の改変を意味する。
日本コミュニティにおけるmod開発は、modを作りたい人と既にmodを作った人の間で技術を伝えることによって成立してきた部分があるのだが、一般に情報が開示されてこなかったので、ここにごく簡単なmodの製作方法をまとめる。
目指せmodder!



必須知識

自分が作りたいmodに応じて、身につけておかないといけない知識がある。

ブロックを作る場合

ブロック自体は公式が用意したモジュールを利用することで、プログラミングをせずとも実装可能。しかし、モジュールだけでできることはかなり少なく、えてしてコードを書くことになる。Add Custom Module modを用いればモジュール記述を増やすことができるが、これについてはACMmod解説を参照してほしい。
- Blender等の3Dモデリング知識

エンティティを作る場合

エンティティとはレベルエディタでおけるオブジェクトのこと。エンティティはブロックと同じく基本的にプログラミング無しで実装できるが、こちらは後からスキンを変えることができない。
- Blender等の3Dモデリング知識

プログラミングをする場合

ほとんどのmodではプログラミングをすることになる。このページではこの部分について詳しく述べる。
- C#の基礎的な知識
- Unityの基礎的な知識
- 高校レベルの英語力(情報源が英語なため)
Unityのバージョンは5.4なので、事前にやりたいことがこのバージョンでできるか確認すること。足りないものがあれば各自勉強して身に着けてほしい。

また、主に次のソフトが必要。
- Visual Studio等のエディタ
- ILSpy等の逆コンパイラ
- Unity 5.4
エディタはプログラミングをするので当然必須。このページでは逆コンパイラを使うことはないが、実際にmodを作る際には公式のアセンブリや、他のmodのソースを見てやり方を勉強しないと現実的にやっていけない。Unityはブロックのエフェクトや海などのアセットを用意する場合に必要だが、全てのmod製作に必要なわけではない。

より詳細な情報

このページはあくまでチュートリアルなので、実用的なmodを作るまでは自分で開発することになる。
もっと多くの情報を得たい場合は以下の方法を試してみてほしい。
  • 公式が公開しているリファレンス(リンク)(英語)
  • http://mod.besiege.co.uk/api/Modding.html(英語)
  • 公式Discord #modding チャンネルで質問する(英語)
  • 公式Discord #japanese-besiege チャンネルで質問する(日本語)


modを作る

おおもとのmodファイルを生成する。
Besiegeを開始して、どの画面でもいいのでCtrl+Kを押す。すると、コンソール画面が出現する。
コンソールはコマンドを打ってBesiegeの設定をなんやかんやする(適当)ことができるのだが、ここで以下のようにコマンドを打つ。
createmod wiki_tutorial01
すると、Besiege_Data/Mods/にwiki_tutorial01 Projectというmodプロジェクトが自動で生成される。これでwiki_tutorial01というmodを作成することができた。
createmodは、直後に作りたいmodの名前を入力することで、そのmodを作成するコマンドである。

次に、このmodにC#のアセンブリを追加したいので、次のようにコマンドを打つ。
createassembly wiki_tutorial01 compiled tutorial01_assem Tutorial01Space
すると、wiki_tutorials01 modに、tutorial01_assemという名前のアセンブリが、Tutorial01Spaceという名前空間で生成される。アセンブリ名と名前空間はただの名前なので何を入れても良い。まよったら自分のユーザ名でも入れておけばよい。

詳細な説明は割愛するが、コンソール上で
createblock hogehoge
と打てばhogehogeというブロックを、
createentity hogehoge
と打てばhogehogeというエンティティを作成することができる。

メニュー画面のmodリストを見ると、次のようにmodが生成されているはずである。
次の章からプログラムを書いてビルドしてゲーム内の挙動を確かめて…とするが、modをビルドする度にBesiegeを再起動する必要があることに注意しよう。

Hello World!

Visual Studioで
Besiege_Data/Mods/wiki_tutorial01 Project/src/wiki_tutorial01.sln
を開く(何も表示されない場合は/src/tutorial01_assem/Mod.csも開く)。すると次のようにModクラスが書かれている。
これがmod作成時の初期画面になる。Modクラスについて簡単に説明すると、Modクラスはこのmodの基地のようなクラスで、例えばこのクラスで生成したGameObjectを親として、UIやら機能やらのスクリプトを付けたGameObjectを紐づけていくことになる。ちなみにクラス名を変えるとmodが認識されなくなる。
このままCtrl+Bでソリューションをビルドすると、Besiege内でmod loaderがビルドしたdllファイルを読み込んでmodとして使えるようになる。ただし、まだ何も機能を付けていないので、modがロードされたかどうか自体わからない。
ここで、OnLoad関数の中に以下のように記述してみよう。

  1. using System;
  2. using System.Collections.Generic;
  3. using Modding;
  4. using Modding.Blocks;
  5. using UnityEngine;
  6.  
  7. namespace Tutorial01Space
  8. {
  9. // modの拠点となるクラス
  10. public class Mod : ModEntryPoint
  11. {
  12. // ステージ(バレンエクスパンスなど)に初めて入った時にmodがロードされ、同時にOnLoad()が呼び出される。
  13. public override void OnLoad()
  14. {
  15. // コンソールに「Hello World!」と表示させる。
  16. ModConsole.Log("Hello World!");
  17. // Debug.Log("Hello World!"); でも可
  18. }
  19. }
  20. }
  21.  
ModConsole.Log()は、Besiegeのコンソールに入力したstring値を表示する関数である。
Unityユーザに取ってはDebug.Log()がなじみ深いと思うが、Debug.Log()でも同じことができる。Debug.LogWarningとDebug.LogErrorは色が変わって表示される。
これを書いてからビルドし、Besiegeを起動してどこかのシーンに入ると、コンソールに次のように表示される。
moddingの世界へようこそ。

+ おまけ
おまけ
modをいじっていると、自分のmodがどこで何をしたのかプリント文デバッグすることが多いのだが、別のmodを入れながら作業しているとどのプリント文がどのmodから発せられているかわからなくなる場合があるので、メッセージの中にはmod名を書いておくことを推奨する
  1. public class Mod : ModEntryPoint
  2. {
  3. public static void Log(string msg)
  4. {
  5. // ModConsole.Log(...) と書いても同じ
  6. Debug.Log("wiki tutorial : " + msg);
  7. }
  8. public static void Warning(string msg)
  9. {
  10. Debug.LogWarning("wiki tutorial : " + msg);
  11. }
  12. public static void Error(string msg)
  13. {
  14. Debug.LogError("wiki tutorial : " + msg);
  15. }
  16. }
  17.  


GUIの追加

modといえばグラフィカルユーザインターフェースである。これを追加していこう。
そのためには、「GUIを制御するためのクラス」が必要になる。まずはこれを作る。
※これ以降は特別な記述がない限りusingとnamespace Tutorial01Spaceの記述を省きます。
  1. public class Gui : SingleInstance<Gui>
  2. {
  3. // 変数
  4. private Rect windowRect = new Rect(0, 80, 200, 100);
  5. private int windowId;
  6.  
  7. // プロパティ
  8. // SingleInstance<>の継承で必要だが、どんな名前でもよい
  9. public override string Name
  10. {
  11. get
  12. {
  13. return "wiki tutorial gui";
  14. }
  15. }
  16.  
  17. // メソッド
  18. public void Awake()
  19. {
  20. // 他のmodのGUIと競合しないwindow IDを生成する(呪文だと思ってOK)
  21. windowId = ModUtility.GetWindowId();
  22. }
  23.  
  24. // GUIの値が変化するフレームでのみ呼び出される
  25. // そのため、Updateよりも計算量が少なくなる
  26. public void OnGUI()
  27. {
  28. // マルチバース参加者ではなく、タイトル画面ではない場合
  29. // すなわち、一人プレイ時またはサーバーホスト時でマップに入っている場合にUIを表示する
  30. if (!StatMaster.isClient && !StatMaster.isMainMenu)
  31. {
  32. windowRect = GUI.Window(windowId, windowRect, delegate(int windowId)
  33. {
  34. // GUIの中身を構成する
  35.  
  36. // 文字を表示させる
  37. GUILayout.Label("Mod完全に理解した");
  38. GUI.DragWindow();
  39. }
  40. , "wiki tutorial");
  41. }
  42. }
  43. }
  44.  
SingleInstance<Type>の継承は呪文だと思ってほしい。Nameというプロパティを継承する必要があるが、これは適当に値を入れておけば良いし、別段どこかで使うわけでもない。
windowRect変数はUIの初期位置と初期サイズを決める。
OnGUI関数はコメントでも書いているが、Updateより計算量が少なくなるGUI専用のメソッドである。
GUIの中身はdelegate(int windowId){}の中に記述することになる。ここでは単純にラベルを表示させるようにしている他、GUI.DragWindow()メソッドを末尾に入れることで、マウスドラッグでUIを動かせるようにしている。UIのタイトルはそのあとにある「"wiki tutorial"」部分である。
このクラスはModクラスでまだ呼び出されていないので、Modクラスも書き換える必要がある。
  1. // modの拠点となるクラス
  2. public class Mod : ModEntryPoint
  3. {
  4. // 変数
  5. GameObject mod;
  6.  
  7. // メソッド
  8. // ステージ(バレンエクスパンスなど)に初めて入った時にmodがロードされ、同時にOnLoad()が呼び出される。
  9. public override void OnLoad()
  10. {
  11. // コンソールに「Hello World!」と表示させる。
  12. ModConsole.Log("Hello World!");
  13.  
  14. // modゲームオブジェクトを初期化し、GUI等で使うクラスのインスタンスを子に指定する。
  15. mod = new GameObject("WikiTutorialController");
  16. SingleInstance<Gui>.Instance.transform.parent = mod.transform;
  17.  
  18. // シーンをまたいでもmodが消されないようにする。
  19. UnityEngine.Object.DontDestroyOnLoad(mod);
  20. }
  21. }
  22.  
modというGameObjectはGuiクラスのインスタンスを子として持つだけの空オブジェクトである。シーンをまたいでも消えないようにDontDestroyOnLoad(mod)としている。このSingleInstance<>回りの記述はどこのmodでも同じように書いているおまじないみたいなものなので、この通りに書こう。

ここまでうまく書けていれば、表題のようなUIが表示されているはずだ。


ブロックにスクリプトを貼り付ける

エンハンスmodや海mod、スタビmod、ABSなど、ありとあらゆるmodが既存ブロックにスクリプトを貼り付けることで動作している。入門用ページとしてはこれを通らない手はない。

ブロックにスクリプトを貼り付けるには、一般的に以下の方法をとる。
  • 貼り付けたいスクリプトを用意する
  • 貼り付けたいスクリプトと、貼り付けたいブロックを紐づけた辞書を設定する
  • ブロックが設置された場合に、そのブロックを対象に以下の関数を適用するように挙動を追加する
  • もしそのブロックが、自分がスクリプトを貼り付けたいブロックなら、辞書にしたがってスクリプトを貼り付ける

何を言っているかわからないかもしれないので、実際にコードを見てもらう。これは上の箇条書きのうち一番上以外である。
  1. using System;
  2. using System.Collections.Generic; // これを追加
  3. using Modding;
  4. using Modding.Blocks; // これを追加
  5. using UnityEngine;
  6.  
  7. // ブロックにスクリプトを追加するクラス
  8. public class BlockSelector : SingleInstance<BlockSelector>
  9. {
  10. // 変数
  11. // ブロックのIDと、追加したいスクリプトを紐づけた辞書
  12. public Dictionary<int, Type> BlockDict = new Dictionary<int, Type>
  13. {
  14. // スタートブロック
  15. {0, typeof(StartingBlockScript) },
  16. };
  17.  
  18. // プロパティ
  19. // Guiと同様の呪文
  20. public override string Name
  21. {
  22. get
  23. {
  24. return "wiki tutorial BlockSelector";
  25. }
  26. }
  27.  
  28. // メソッド
  29. public void Awake()
  30. {
  31. // ブロックを設置した場合に呼び出されるアクションに、AddScriptというメソッドを追加する
  32. Events.OnBlockInit += new Action<Block>(AddScript);
  33. }
  34.  
  35. // ブロック設置時に、そのブロックに所定のスクリプトを貼り付ける関数
  36. // Blockは、設置したブロックを表す
  37. public void AddScript(Block block)
  38. {
  39. // 生成したブロックのBlockBehaviourコンポーネントを取得する
  40. BlockBehaviour internalObject = block.BuildingBlock.InternalObject;
  41.  
  42. // そのブロックがスクリプトを貼り付けるべきブロックであるなら、貼り付ける
  43. if (BlockDict.ContainsKey(internalObject.BlockID))
  44. {
  45. Type type = BlockDict[internalObject.BlockID];
  46. try
  47. {
  48. // まだ所定のスクリプトが貼り付けられていない場合にのみ、貼り付ける
  49. if (internalObject.GetComponent(type) == null)
  50. {
  51. internalObject.gameObject.AddComponent(type);
  52. Mod.Log("Added Script");
  53. }
  54. }
  55. catch
  56. {
  57. Mod.Error("AddScript Error!");
  58. }
  59. return;
  60. }
  61. }
  62. }
  63.  
この記述はABSに書かれているものと同じ最小構成で、エンハンスmod等はエラーを極力排除するようにより複雑な記述になっている。実際は辞書部分以外をエンハンスmod等の既存modから持ってくる場合が多いだろう。

そして、実際に貼り付けたいスクリプトを、例えばこんな風に書いてみる。これは、スタートブロックの設定によりGUIに文字を表示させたりさせなかったりするためのスクリプトである。先ほど生成したGUIとさっそく連携させる狙いがある。Guiクラスも書き換える必要があるので、Guiクラスも書いておく。
  1. // スタートブロックに貼り付けるスクリプト
  2. public class StartingBlockScript : MonoBehaviour
  3. {
  4. // 変数
  5. public BlockBehaviour BB;
  6. public MToggle toggle;
  7.  
  8. // メソッド
  9. private void Awake()
  10. {
  11. // BlockBehaviourを取得
  12. BB = GetComponent<BlockBehaviour>();
  13.  
  14. // ブロックの設定画面にトグルを追加
  15. toggle = BB.AddToggle("toggle", "wiki tutorial toggle", false);
  16. }
  17.  
  18. private void Update()
  19. {
  20. // GUIで文章を表示するかを、トグルの値に応じて変える
  21. SingleInstance<Gui>.Instance.StartingBlockToggle = toggle.IsActive;
  22. }
  23. }
  24.  
  25. // GUIで用いるクラス
  26. // SingleInstance<>を継承するのは呪文だと思って貰えばOK
  27. public class Gui : SingleInstance<Gui>
  28. {
  29. // 変数
  30. private Rect windowRect = new Rect(0, 80, 200, 100);
  31. private int windowId;
  32. public bool StartingBlockToggle = false;
  33.  
  34. // プロパティ
  35. // SingleInstance<>の継承で必要だが、どんな名前でもよい
  36. public override string Name
  37. {
  38. get
  39. {
  40. return "wiki tutorial gui";
  41. }
  42. }
  43.  
  44. // メソッド
  45. public void Awake()
  46. {
  47. // 他のmodのGUIと競合しないwindow IDを生成する(呪文だと思ってOK)
  48. windowId = ModUtility.GetWindowId();
  49. }
  50.  
  51. // GUIの値が変化するフレームでのみ呼び出される
  52. // そのため、Updateよりも計算量が少なくなる
  53. public void OnGUI()
  54. {
  55. if (!StatMaster.isClient && !StatMaster.isMainMenu)
  56. {
  57. windowRect = GUILayout.Window(windowId, windowRect, delegate(int windowId)
  58. {
  59. // GUIの中身を構成する
  60.  
  61. // 文字を表示させる
  62. GUILayout.Label("Mod完全に理解した");
  63.  
  64. // スタートブロックのトグルが押されていれば、これも表示する
  65. if (StartingBlockToggle)
  66. {
  67. GUILayout.Label("スタブロと和解せよ");
  68. }
  69.  
  70. GUI.DragWindow();
  71. }
  72. , "wiki tutorial");
  73. }
  74. }
  75. }
  76.  
BlockBehaviour.AddToggle関数はブロックの設定画面にトグルを追加する。他にもAddMenuとか似たような関数が色々あるので、もっと知りたい人は公式のドキュメントを読んでみよう。
Guiクラスには、StartingBlockScriptからアクセス可能なStartingBlockToggleというbool型変数と、それに応じて表示するか変わるGUILayout.Label関数を追加している。


C#コーディングまとめ

以上を総合すると、全体のMod.csは次のようになる。
+ Mod.cs全文
Mod.cs全文
  1. using System;
  2. using System.Collections.Generic;
  3. using Modding;
  4. using Modding.Blocks;
  5. using UnityEngine;
  6.  
  7. namespace Tutorial01Space
  8. {
  9. // modの拠点となるクラス
  10. public class Mod : ModEntryPoint
  11. {
  12. // 変数
  13. GameObject mod;
  14.  
  15. // メソッド
  16. // ステージ(バレンエクスパンスなど)に初めて入った時にmodがロードされ、同時にOnLoad()が呼び出される。
  17. public override void OnLoad()
  18. {
  19. // コンソールに「Hello World!」と表示させる。
  20. ModConsole.Log("Hello World!");
  21.  
  22. // modゲームオブジェクトを初期化し、GUI等で使うクラスのインスタンスを子に指定する。
  23. mod = new GameObject("WikiTutorialController");
  24. SingleInstance<Gui>.Instance.transform.parent = mod.transform;
  25. SingleInstance<BlockSelector>.Instance.transform.parent = mod.transform;
  26.  
  27. // シーンをまたいでもmodが消されないようにする。
  28. UnityEngine.Object.DontDestroyOnLoad(mod);
  29. }
  30.  
  31. // このようなプリント系メソッドを用意しておくと他modのメッセージと混同しにくい。
  32. public static void Log(string msg)
  33. {
  34. // ModConsole.Log(...) と書いても同じ
  35. Debug.Log("wiki tutorial : " + msg);
  36. }
  37. public static void Warning(string msg)
  38. {
  39. Debug.LogWarning("wiki tutorial : " + msg);
  40. }
  41. public static void Error(string msg)
  42. {
  43. Debug.LogError("wiki tutorial : " + msg);
  44. }
  45. }
  46.  
  47. // GUIで用いるクラス
  48. // SingleInstance<>を継承するのは呪文だと思って貰えばOK
  49. public class Gui : SingleInstance<Gui>
  50. {
  51. // 変数
  52. private Rect windowRect = new Rect(0, 80, 200, 100);
  53. private int windowId;
  54. public bool StartingBlockToggle = false;
  55.  
  56. // プロパティ
  57. // SingleInstance<>の継承で必要だが、どんな名前でもよい
  58. public override string Name
  59. {
  60. get
  61. {
  62. return "wiki tutorial gui";
  63. }
  64. }
  65.  
  66. // メソッド
  67. public void Awake()
  68. {
  69. // 他のmodのGUIと競合しないwindow IDを生成する(呪文だと思ってOK)
  70. windowId = ModUtility.GetWindowId();
  71. }
  72.  
  73. // GUIの値が変化するフレームでのみ呼び出される
  74. // そのため、Updateよりも計算量が少なくなる
  75. public void OnGUI()
  76. {
  77. if (!StatMaster.isClient && !StatMaster.isMainMenu)
  78. {
  79. windowRect = GUILayout.Window(windowId, windowRect, delegate(int windowId)
  80. {
  81. // GUIの中身を構成する
  82.  
  83. // 文字を表示させる
  84. GUILayout.Label("Mod完全に理解した");
  85.  
  86. // スタートブロックのトグルが押されていれば、これも表示する
  87. if (StartingBlockToggle)
  88. {
  89. GUILayout.Label("スタブロと和解せよ");
  90. }
  91.  
  92. GUI.DragWindow();
  93. }
  94. , "wiki tutorial");
  95. }
  96. }
  97. }
  98.  
  99. // ブロックにスクリプトを追加するクラス
  100. public class BlockSelector : SingleInstance<BlockSelector>
  101. {
  102. // 変数
  103. // ブロックのIDと、追加したいスクリプトを紐づけた辞書
  104. public Dictionary<int, Type> BlockDict = new Dictionary<int, Type>
  105. {
  106. // スタートブロック
  107. {0, typeof(StartingBlockScript) },
  108. };
  109.  
  110. // プロパティ
  111. // Guiと同様の呪文
  112. public override string Name
  113. {
  114. get
  115. {
  116. return "wiki tutorial BlockSelector";
  117. }
  118. }
  119.  
  120. // メソッド
  121. public void Awake()
  122. {
  123. // ブロックを設置した場合に呼び出されるアクションに、AddScriptというメソッドを追加する
  124. Events.OnBlockInit += new Action<Block>(AddScript);
  125. }
  126.  
  127. // ブロック設置時に、そのブロックに所定のスクリプトを貼り付ける関数
  128. // Blockは、設置したブロックを表す
  129. public void AddScript(Block block)
  130. {
  131. // 生成したブロックのBlockBehaviourコンポーネントを取得する
  132. BlockBehaviour internalObject = block.BuildingBlock.InternalObject;
  133.  
  134. // そのブロックがスクリプトを貼り付けるべきブロックであるなら、貼り付ける
  135. if (BlockDict.ContainsKey(internalObject.BlockID))
  136. {
  137. Type type = BlockDict[internalObject.BlockID];
  138. try
  139. {
  140. // まだ所定のスクリプトが貼り付けられていない場合にのみ、貼り付ける
  141. if (internalObject.GetComponent(type) == null)
  142. {
  143. internalObject.gameObject.AddComponent(type);
  144. Mod.Log("Added Script");
  145. }
  146. }
  147. catch
  148. {
  149. Mod.Error("AddScript Error!");
  150. }
  151. return;
  152. }
  153. }
  154. }
  155.  
  156. // スタートブロックに貼り付けるスクリプト
  157. public class StartingBlockScript : MonoBehaviour
  158. {
  159. // 変数
  160. public BlockBehaviour BB;
  161. public MToggle toggle;
  162.  
  163. // メソッド
  164. private void Awake()
  165. {
  166. // BlockBehaviourを取得
  167. BB = GetComponent<BlockBehaviour>();
  168.  
  169. // ブロックの設定画面にトグルを追加
  170. toggle = BB.AddToggle("toggle", "wiki tutorial toggle", false);
  171. }
  172.  
  173. private void Update()
  174. {
  175. // GUIで文章を表示するかを、トグルの値に応じて変える
  176. SingleInstance<Gui>.Instance.StartingBlockToggle = toggle.IsActive;
  177. }
  178. }
  179. }
  180.  
長いけどできることはほぼないから悲しいね。でかいmodは数千行数万行とコードが膨大になるので、野望に見合ったやる気が必要。頑張れ。


Mod.xmlの編集

Mod.xmlとは、modの情報をまとめたxmlファイルであり、Besiege起動時に読み込まれるmodの仕様書みたいなもので、createmodした時に自動で作成される。mod作成時には「Mod.xmlを編集しなさい」と言われるのだが、このサンプルmodの動作にはあまり影響が無い。modを公開する時に編集必須になる。
要素をいちいち説明すると長くなってしまうので割愛するが、下記のソースコードからDLできるサンプルmodのMod.xmlには、英語で書かれた説明文に加えてその和訳も付けておいたので、代わりにそちらを読んで欲しい。訳者註も付けたのでわかりやすいと思う。


ソースコード

このチュートリアルで作ったmodのサンプルは以下のGitHubページからダウンロードできる。
https://github.com/Yamabach/wiki_tutorial01
このmodをそのまま、または改変してワークショップにアップロードすることは固く禁止する(同じmod IDの別modが存在すると何が起こるかわからないため)。あくまで勉強用に使ってください。


タグ:

+ タグ編集
  • タグ:
最終更新:2024年04月07日 00:29
|新しいページ |検索 |ページ一覧 |RSS |@ウィキご利用ガイド |管理者にお問合せ
|ログイン|