BF6 「Portal」の作り方解説 – カスタムのゲームモードをゲームエンジン「ゴドー」とGUIプログラミングで作る方法

表題の通りです。

動画作ったので↓ご覧ください。


ファイル一覧:
・ガイドドキュメント markdown
 → https://www.mediafire.com/file/h22co7ll2n0h22f/PORTAL_DOC.md/file
・Limestoneのspatial.json
 → https://www.mediafire.com/file/jmkn6dq118bpw6a/MP_Limestone.spatial%25281%2529.json/file
・ブロックエディタのworkspace.json
 → https://www.mediafire.com/file/5pn0ksp2wau5gqi/small_tdm_init_workspace.json/file


目次

コミュニティドキュメント

https://docs.bfportal.gg/santiago/scripting/mod/mod

Portal でのポイント

動画でも述べていますがつまづきやすいポイントをまとめました。

チーム人数は必ず2人以上に設定

1人だと0人になってしまうようです。もしかしたら観戦者用スロット扱いとかなのかもしれません。

マップインポート時は必ず「Include Default Spatial」にチェック

挙動が安定しないのかな?と最初思っていたのですが、どうやら再インポートする際などは勝手にチェックが外れるようで、その状態でインポートすることで崩れたりしていたみたいです。
そのため、一度削除して再インポートする場合は必ずチェックを入れるように意識したほうがいいかと思われます。
→2025-10-26 追記: 挙動がいまいち不明なので、結局よくわかっていません。

Godot でのセットアップ方法

  1. 「_sc_」という空のファイル(拡張子なし)をexeファイルがあるところに置いておく。そうすると完全ポータブルモードになってあとからでも動かしやすくなる。
  2. 公式のアドオンがあるので、recolorizerだけでも入れておく。(ほかは任意。慣れてきたらでOK)
    (リンクは後述)
  3. Godot を起動
  4. Godot でSDK同梱(どうこん)のプロジェクトを開く
  5. 待つ
  6. 右パネルの「Portal Setup」を押す
  7. 待つ
  8. メニュ-の「シーン」→「シーンを開く」または左のパネルからlevelsの中の項目をどれかひとつ開く
  9. 下パネルの Object Library を開いてそこからドロップ
公式動画のメモ

7 Godot Add-ons for Battlefield Portal MAP Creation
https://www.youtube.com/watch?v=YRh_70gmXV8

https://drive.google.com/drive/folders/1fSuUywsBAxw2wMQRZ2Id0nWKrlKfB83I

  • gibbファイルは配置できない。tsnとかだと置ける。
  • 大きさの縦横比変えると正常に表示されないらしい。
  • Node3Dはメモ用らしい。
アドオンの説明

画面上部のAssetLibのタブからアドオンをインストール可能。

名前配布説明画像
Recolorizerオブジェクトはデフォで白いので見分けるために使用
MagnetSnapVキーでオブジェクト同士にスナップ可能。
後述のGodot標準機能で代替可能
Spacer間隔を空けるやつ
Duplicator3D配列配置
Alignmentオブジェクトを整列可能。
ProtonScatterいい感じにオブジェクトを散らばすやつ。
シーンパネルから追加。オブジェクトのパスを右クリから取得し、Pathに設定。
ただしエクスポート対象にできないので、アセットとしてコピーを実際にレンダリングする設定にして消すこと。
PhysicsPlacer↑の実際に物理演算させるVer.
落として散らばす感じ。
マップ対応表
Map/Level NameLevel fileロケーション備考サイズ
Empire State
エンパイア・ステート
MP_Aftermathブルックリンベータありふつう
Manhattan Bridge
マンハッタン橋
MP_Dumboブルックリンふつう
Liberation Peak
リベレーションピーク
MP_Capstoneタジキスタンベータあり広い
Mirak Valley
ミラ・バレー
MP_Tungstenタジキスタン広い
New Sobek City
ニュー・ソベクシティ
MP_Outskirtsエジプト・カイロふつう
Siege of Cairo
カイロ包囲戦
MP_Abbasidエジプト・カイロベータありふつう
Operation Firestorm
(ファイアストーム作戦)
MP_FirestormトルクメニスタンBF3マップ広い
Iberian Offensive
イベリア攻勢
MP_Batteryジブラルタルベータあり広い
Saint’s Quarter
聖人地区
MP_Limestoneジブラルタル狭い
Godot で使えるオブジェクト
名称(英語)説明(日本語)用途 / 備考
Combat Areaマップ内でプレイ可能な領域(戦闘エリア)を定義するPolygonVolume を使い、頂点を編集して範囲を描く。外へ出ると警告または死亡処理になる
HQ_PlayerSpawnerチーム専用のHQスポーン(本拠地スポーン)出撃画面でHQを選んでスポーンできる。InspectorでTeam IDを設定必須
PlayerSpawnerチームに属さない汎用スポーンポイントスクリプトから直接 Teleport / Deploy させる用途に向く(HQが無くても可)
SpawnPointHQ_PlayerSpawnerPlayerSpawner に接続される実際の出撃位置HQやSpawnerにリンクしないと無効。位置(座標)だけを示すオブジェクト
AreaTriggerPolygonVolume などと組み合わせて、侵入・退出イベントを発火させるOnPlayerEnterAreaTrigger / OnPlayerExitAreaTrigger でスクリプト制御可能
DeployCamデプロイ画面(出撃前マップ)で使われるカメラロビーで表示されるマップの角度・ズームを決める。上面図にしたい場合はここに真俯瞰カメラを置く
VehicleSpawnerスクリプトでビークルを湧かせるトリガーとして使うTriggerVehicleSpawner などで呼び出せる
WorldIconワールド空間にピンやテキスト表示を出すためのオブジェクトHQピン・目的地マーカーなどに利用可能
InteractPointプレイヤー操作に反応するポイント(例:押す、起動する)スクリプトで OnPlayerInteract 的な処理が可能
AI_SpawnerカスタムAI用のスポーンポイントSpawnBot() 系スクリプトでここからBotを出現させる
CapturePointコンクエ等フラッグPolygonVolume と一緒に使って「Capture Area」プロパティで領域を設定する。
PolygonVolumeエリア設定コンクエキャプチャエリアや戦闘領域設定。
Ctrl+クリックで線を追加可能。
マップカメラは「DeployCam」を配置

出撃時の全体マップはゴドー上で DeployCam を配置しておきましょう。
それでも若干ズレてる気がしますが、本編(マルチプレイヤー)でもズレてるのでたぶん全体的なバグだと思います。

tscn(シーン)ファイルが読み込めなくなったとき

scene/resources/resource_format_text.cpp:39 – res://levels/MP_Limestone.tscn:23 – Parse Error: Expected ‘,’ or ‘)’ in constructor.
と言われたとき
tscnファイルをテキストエディタで読み込んで(ファイルサイズが大きくても読めるはず)
該当の行に飛ぶ。この例だと23行目。
で、その前の行の最後を確認してみる。
自分の例ではなぜか最後の ) が取れていたので付け加えて保存したらいけた。
あとはスナップ用にスタティックメッシュを作ると結構壊れやすい(あと重くなる)。
なのでスタティックメッシュのとんでもない量の座標データごと削除して一旦読み込める場合もある。
そのためコリジョンのスタティックメッシュは作ったら閉じる前に消して「編集可能な子」を戻しておくのがおすすめ。

2025-10-16 追記 オブジェクトのスナップ

オブジェクトをスナップさせて配置させたい場合、
左のシーンパネルからStatic > (マップ名)~_Terrain を右クリックし「□Editable Children」を有効化。できたMeshに対しさらにもう一度実行。
出てきたTerrainを選択すると3Dビュー上部にMeshという項目が出てくる。
これを押して
「Create Collision Shape」>「Sibling(兄弟)」を「Static Body Child(スタティックボディの子)」に変更する。

こうすると自動でオブジェクトが地形にスナップするようになる。
~_Assetsも同様にすると薄いオレンジの建物にもスナップされる。
この状態で Shift+G を押すとスナップモードでの配置が可能。

※ファイル重くなるので閉じるときは先に元に戻してから(□Editable Childrenをチェック外してから)。

ビークルについて

どうやら普通にビークルそのものを置いてもいいらしい。
(勘違いだったかも)

任意のオブジェクトを置くことは可能か?

適当なtscnのオブジェクトファイルを開いて、Meshを「編集可能な子」にして、右のインスペクタパネルでメッシュを「読み込み」して、Objファイルなどを適当に選択できればワンチャンあり?すでにボックスとかテキストとか基本形状ならいける。(3DテキストはWorldIconもあるがこっちのほうが楽かも?)


ロジック実装

ロジック実装は「HELP」を見る

Rules Editor のブロックエディタではすぐに右クリックするクセをつけましょう。

コンクエの作り方
  • HQをそれぞれマップ全域くらいに広げる
  • Team1とかの設定はそのまま
  • 以下を配置する
    • Sector CapturePoints に↓をすべて追加
    • CapturePointA Area割当、SpawnPoint割当をする
      • PolygonVolume 必要。ある程度大きくしておく
      • WorldIcon AとかBとか。Visibleにもチェック。たぶん必要
      • DeployCam 適当に。
      • AI_WaypointPath 必要かもしれない……
      • PlayerSpawner 必要。SpawnPoint割当。
        • SpawnPoint 必要。
        • SpawnPoint2 必要。

※GetProgress系はもしかしたらバグで動いてないかも?

戦闘エリア外になるとき

→HQを広げる。お互いに入らせたくないところだけ(相手チームの復活ポイントなど)
含めず、あとはほぼ全部カバーさせる。

ロジック追加は必ずひとつずつ!

→どこかにエラーがあると全体が動かなくなるため、どれが原因なのかなんで動かないのかわからなくなる。

オブジェクトを数字で呼び出すとき

配列ではなくObj ID指定。

TypeScript でのコツ:

基本形:
 import
 定義(const, let, var)
 export function 関数名( 引数1:型名, 引数2: 型名 )
 {
 }

  • SDK の mod フォルダに書こう。拡張子は .ts で。
  • Basic_Template を見よう
  • 最初に↓を書いておくと Visual Studio で補完しながらコーディングできるぞ!
    • import * as modlib from 'modlib';
  • デフォルトの関数やイベントは export function ~~ と書く。
  • 何の関数があるのか等はSDKのどっかに「index.d.ts」というファイルがあるのでそれを Ctrl+F とかで検索する。
  • Visual Studio(VS)では{}を新しい行にして、()の中の前後に空白を入れるとわかりやすいぞ!(設定ごと変える)
  • ラムダ式や三項演算子( (条件) ? AAA : BBB ) が使えるぞ!asyncも!
  • バトルフィールド6で使うほとんどは mod. ~~~~ の形式で呼び出すぞ!
    (それ以外の計算類はそのまま TypeScript が使える)
  • 値への代入はプレイヤー空間なら
    mod.SetVariable( mod.ObjectVariable( eventPlayer, mod.変数名 ), 値 );
  • 値のGetは mod.GetVariable( mod.ObjectVariable( eventPlayer, mod.変数名 ) ); とする!安易に mod.変数名 = ~~~ とすると Global ではいいが Player 空間などでは個別の値にならずNGらしい。
    • とはいうがめんどくさいので player 系のイベントだったら GetObjId でID取ってそれ次第で普通の書き方で処理回すのが一番スマートかも?
  • Webブラウザ上で書くのではなく、VSで書いたファイルをアップロードし、string.json は生成しないほうがいいかも?
  • tsファイルを書いたら Generate string json from script ボタンで必ず json を生成する。そうでないと string 系が全滅する。
  • Message に string + number は入らない。コッソリ エラーになる。(本当にやめてほしい。かと思えば入るときもあり条件不明)
    • どうやら mod.Message( "{} {}", numberValue, m ) だと 「15 m」的な感じで合成できるらしい。ただし中に演算系の記号があると結構厳しめ?
  • 日本語NG。コメントでもNG。
  • どうやら一行コメントは英数字記号であってもだめなことがあるらしい。
    • その場合は (スラッシュ+アスタリスク)/* */ による複数行コメントならOK?
  • string.json を必ず再生成すること(二度目)
  • async function にして await Wait(秒数) で処理を待機可能。ただし Ongoing系では使えないこともあり。
  • コンパイルエラー基本出ないのでセミコロン;忘れとか注意!
  • 残念ながら「:」(コロン)は使えない。Parser の影響だろうか?どうしても使いたいなら string.json にて [colon]: ":" とでも定義しておき、[colon] をMessage等で呼び出すとコロンに自動置換される。
  • AI系メソッド等については「AISpawner経由で手動スポーンさせたもののみ有効」なので注意。(BotのBackfillやStaticはコントロールできない)
  • ongoing で毎フレームごとに処理すると7268で止まるかも。2フレームとか工夫する

機能しないファンクション、バグなど

距離を取得する

デフォルトのDistanceBetweenがバグなのか死んでるのと、EventPlayerが全プレイヤーに対して処理を行ってしまうのでなかなかうまくいかない。
だがとりあえず2D的に距離を取得するなら、pがプレイヤーの位置、cが対象の位置だとすれば、

dx = px - cx, dz = pz - czdist2D = Sqrt(dx*dx + dz*dz)

でOK。ブロックは大変長くなるので省略。SqrtはSquareRoot。
あとはこれをサブルーチンで計算してdist2DにSetVariableでもすれば呼び出せる。

CapturePoint の進捗状況取得

→2025-10-20、GetCapturePointProgress というのはあるが機能せず。

CapturePoint にいるプレイヤー取得

mod.PlayersOnPoint() は不安定で、機能しないときがある。
代わりに↓を使用する。

const CAPTURE_RADIUS_M = 9; 
const HEIGHT_TOLERANCE_M = 6;

function playersInCapturePoint( cp: mod.CapturePoint ): mod.Player[]
{
    const list: mod.Player[] = [];
    const all = mod.AllPlayers();
    const count = mod.CountOf( all );

    const cpPos = mod.GetObjectPosition( cp );
    for ( let i = 0; i < count; i++ )
    {
        const p = mod.ValueInArray( all, i ) as mod.Player;
        if ( !mod.GetSoldierState( p, mod.SoldierStateBool.IsAlive ) ) continue;

        const pPos = mod.GetObjectPosition( p );
        const dx = mod.XComponentOf( pPos ) - mod.XComponentOf( cpPos );
        const dz = mod.ZComponentOf( pPos ) - mod.ZComponentOf( cpPos );
        const dy = mod.YComponentOf( pPos ) - mod.YComponentOf( cpPos );

        const dist2D = Math.sqrt( dx * dx + dz * dz );
        if ( dist2D <= CAPTURE_RADIUS_M && Math.abs( dy ) <= HEIGHT_TOLERANCE_M )
        {
            list.push( p );
        }
    }
    return list;
}
UIのセンタリングについて

文字も合わせてセンタリングしたいときは、先にSetUITextLabelを設定してから
SetUIAnchor を呼ぶとテキストも一緒にセンタリングされる。
逆にパワーポイントでいうところの「中心」&「左揃え」にしたい場合は先にアンカーを設定する(デフォルトはこっち)。

UI更新について

AddUITextで固定ネームを作ると、全体に反映されるので注意。
たとえばプレイヤーごとに違うUIを出したいなら、AddUITextとかではEventPlayerを明確に指定してやる必要がある(ということはAddUITextは逆説的に OnPlayerDeployed とかの内部でやる羽目になる)。
また、AddUITextでのName指定もかぶると全プレイヤーのUIに対して操作が走ってしまう。そのため、Name時点でEventPlayerごとの独自のIDを振る必要がある。
これがかなり厄介で、デフォのブロックエディターだと数値をstring型にキャストできない=文字列として扱えない、ので自前で最大64個のIDの配列を用意するとかになってしまう……。

そのためできれば早々にTypeScriptの使用を検討したい
(TypeScript側にはあるものがブロックエディタにはないことが多い)

SetUITextではこのUI_IDをFindNameでUIWidgetを探して割り当てる。
Nameの時点で一意になっていればSetUITextはどこで呼ばれてもいいはず。

ただ、While等でやるとめっちゃ重くなって処理が追いつかなかったりするのでWait 0.1 くらいしておく。

※Firefoxの場合、Cookie Auto Deleteアドオンやデフォの設定でプライバシー重視になっていることが悪さをしている場合も。ブラウザを変えたりプライベートブラウジングでアドオンを無効化して起動するのも手。

ブロックエディタでのコンクエなど


↓のEnableGameModeObjectiveを入れないとCaptureなどが機能しない。
CapturePointでコンクエ、
MCOMでラッシュ、
Sectorでブレイクスルー
が自動でONになる。
(あとはそれぞれに応じてオプション設定が必要。
CapturePointとかMCOMとかで左パネルのフィルタ検索して全部突っ込んでおく)

また、必ず↓のように全オブジェクトを配置すること。(DeployCamとかWaypointPathは現在2025-10-26機能不全)

2025-11-08 Update:
出撃時のカメラについて、下記のようにCamera3Dオブジェクトの中に入れるのが正解。
Camera3Dオブジェクトは左上の+マークから検索して追加可能。

PlayerSpawnerにはスポーンポイントを設定。それぞれのチームで。

AIの優先度設定や、
スポーンポイントの設定も必要。

さらにSectorにCapturePointsの設定をする。
画像ではSectorAreaに値が入っていないが、ここにも CombatArea の Volume を指定しておく。
→別々のほうがいいかも?

乗り物一覧
BF6 Portal 兵器一覧(画像付き)

※目視でも確認しましたが、AIのため必ずしも正確とは限りません。

名称兵器種画像
Abrams主力戦車(アメリカ:M1 Abrams)
Leopard主力戦車(ドイツ:Leopard 2)Leopard
Cheetah対空自走砲(ドイツ:Flakpanzer Gepard)
Cv 90歩兵戦闘車(スウェーデン:CV90)
Gepard対空自走砲(ドイツ:Gepard)Gepard
Uh 60汎用ヘリコプター(アメリカ:UH-60 Black Hawk)UH60
Eurocopter多用途ヘリ(仏・独系:Eurocopter/Airbus Tiger等)Eurocopter
Ah 64攻撃ヘリ(アメリカ:AH-64 Apache)AH64
Vector軽装甲車(高速多用途車両/ゲーム固有モデル)Vector
Quadbike四輪バギー(軽車両・偵察用)Quadbike
Golf Cart軽車両(民生改造車)GolfCart
Marauder装甲輸送車(南ア:Marauder MRAP)Marauder
Flyer 60軽戦術車両(アメリカ:Flyer Defense Flyer 60)Flyer60
Jas 39戦闘機(スウェーデン:JAS 39 Gripen)Jas39
F 22戦闘機(アメリカ:F-22 Raptor)F22
F 16戦闘機(アメリカ:F-16 Fighting Falcon)F16
M 2 Bradley歩兵戦闘車(アメリカ:M2 Bradley)
Su 57戦闘機(ロシア:Sukhoi Su-57)Su57
Uh 60 Pax輸送ヘリ(UH-60兵員輸送仕様)UH60Pax
Marauder Pax装甲兵員輸送車(Marauder Pax仕様)MarauderPax
紫のルールの名前は必ず一意に!

→ルール名かぶらせない。New Rule から必ず名前を変える。
または同じルールの使用は1回だけとし、その中ですべてまとめる、とか。

サブルーチンについて

タイプをAnyにすればいちいち意識しなくて済む。
ただ、どうやらサブルーチンそのものを作ったりするたびに既存の
サブルーチン呼び出し先での変数割当が外されてしまったりするので注意。

引数(呼び出し先で同時に渡す値)のサブルーチン内呼び出しはサブルーチンカテゴリの中に 「GetSubroutineArguments」 というのがあるのでそれを使えば引数を呼び出せる。


今回の動画について

今回、動画作成にあたっていくつかチャレンジをしたので何かと至らない部分もあるかもしれませんが、ご了承ください。

なるべく早めにまとめる

発売直後でまだ熱気があるうちにできるだけ早く投稿できるように考えました。
結果としては1週間近く経ってしまいましたが……
土日祝でわりと普通にプレイしていたのと、まさか Godot や TypeScript まで使った本格的なものだとは思っていなかったのと、仕様の理解などに結構時間がかかってしまったのが反省点でした。
(最初は個人的に Firestorm でやっていたりしたのですが、動いたり動かなかったり、マップを読み込めたり読み込めなかったり……結構苦戦していました。)

AIナレーションのテスト

個人的に一番収穫が大きいのはこれでした。
今回、Google AI Studio によるナレーションを使用したのですが、非常にクオリティが高く一部読み間違いなどはあるものの、ここまでできるとは知らなかったので大変勉強になりました。

GPTによるエクスプレッション支援

ChatGPTを使って自動でAEのスクリプト等を組んでもらいました。
これによりめんどくさいもの(今回のインデックスは時間に応じて自動で背景ハイライトがつくようになっています)を数秒で作ってもらえて大変ラクでした。

解説動画の作り方ノウハウ

スクリプト(台本)、ナレーション音声、字幕、実際のデスクトップ動画、ゲーム動画……これらをすべて並行して作ったのですが作りにくいことこの上なかったです……。

理想の本来の作り方は、

  1. スクリプトをすべて整理
  2. 動画のインデックス(目次)の素材をPNGで何枚か作成しておく
  3. ナレーションを作成
  4. ナレーションを Audition で編集しつつつなげる。ロード画面等時間がかかりそうなら分割する
  5. ナレーションを再生しながら実際の画面をキャプチャする。このとき2の工程の素材をOBSであらかじめ合成
  6. ナレーションから字幕を Premiere の自動機能で自動起こし
  7. 字幕やどうしても必要なエフェクトだけ AE を使用してレンダリング or Dynamic Link
  8. 最終レンダリングは Premiere 側から (全然速度が違う)

自分は字幕から動画のカットまですべてAEでやってしまったので、レンダリング時間が3時間とかかかってしまいました。
ただ、編集しやすさは圧倒的に AE のほうがやりやすかったです。
AE は多機能すぎるのが難点ですが、その分慣れると Premiere が使いにくくてしょうがなくなってしまうというメリットなのかデメリットなのかよくわからない状況になります。

ただ、本来はカット編集等メインを Premiere, エフェクトを After Effects と、
ソフト名通りの役割が正しいので、徐々に慣れていくことにしたいです。


もし参考になったなら幸いです。

あ、ちなみにAIナレーションは架空の設定なので本気にしないでください。
(一瞬信じかけちゃうのはよくわかります……なんで本人まで騙されそうになってるのか謎ですが……)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です