この記事では、Unity 2022.3.54f1で「100メートル競争」アプリをゼロから作り、シーン自動構築・ビルド自動化・改善の反映までを、実際に使ったスクリプト/コマンド/ログとともに時系列でまとめます。対象はWindows向けスタンドアロンビルドです。
- プロジェクト名: TestCodex
- Unityエディタ: 2022.3.54f1(エディタのバージョンはたまたまインストールしてあったバージョンです)
- 作業環境: Windows / Unity Hub
- 目的: スフィア・キューブ・カプセルの3体で100m走を行い、最下位にカメラを向けつつ全体を見渡す。Windowsビルドを自動で出力。
重要ポイント: Unityエディタを一度も起動せず、プロンプトとCLIだけで全行程を自動化しています。
追記に今回命令したプロンプトを書き出しました。
1. プロジェクトの作成
- 作成コマンド(Editor CLI)
"C:\Program Files\Unity\Hub\Editor\2022.3.54f1\Editor\Unity.exe" `
-batchmode -nographics -quit `
-createProject "C:\Users\user\Desktop\test\TestCodex" `
-logFile "C:\Users\user\Desktop\test\TestCodex.create.log"
- バージョン確認:
TestCodex/ProjectSettings/ProjectVersion.txt
m_EditorVersion: 2022.3.54f1
m_EditorVersionWithRevision: 2022.3.54f1 (129125d4e700)
リポジトリ
GitHub: https://github.com/kumi0708/TestCodex20251111
2. 空シーン(run100)の作成を自動化
- エディタスクリプト
Assets/Editor/CreateRun100Scene.csを配置し、シーンを作成・保存。 - 実行コマンド
"...\Unity.exe" -batchmode -nographics `
-projectPath "...\TestCodex" `
-executeMethod ProjectSetup.CreateRun100Scene `
-quit -logFile "...\CreateRun100Scene.log"
- 生成シーン:
Assets/Scenes/run100.unity
- 追加スクリプト
-
Assets/Scripts/Runner.cs(ランナー挙動) -
Assets/Scripts/RaceManager.cs(レース管理・UI表示・Escape終了) -
Assets/Scripts/FollowCameraController.cs(最下位注視+全体フレーミング)
-
Runner.cs(抜粋)
public class Runner : MonoBehaviour
{
public string runnerName = "Runner";
public float minSpeed = 6f;
public float maxSpeed = 10f;
public float variability = 0.5f; // 小さな速度揺らぎ
[HideInInspector] public float finishTime = -1f;
RaceManager manager; float baseSpeed; float distanceTravelled; Vector3 startPosition;
public float Progress => distanceTravelled;
public void Initialize(RaceManager mgr) { /* 速度初期化 */ }
void Update() { /* Z+へ前進、100m到達で停止→通知 */ }
}
RaceManager.cs(最終版・全文)
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
public class RaceManager : MonoBehaviour
{
[Header("Race Settings")]
[SerializeField] float raceDistance = 100f;
[Header("References")]
[SerializeField] Text statusText;
public float RaceDistance => raceDistance;
public bool RaceStarted { get; private set; }
public float RaceStartTime { get; private set; }
List<Runner> runners = new List<Runner>();
int finishedCount = 0;
public IReadOnlyList<Runner> Runners => runners;
void Start()
{
runners = FindObjectsOfType<Runner>().OrderBy(r => r.transform.position.x).ToList();
foreach (var r in runners) r.Initialize(this);
StartRace();
}
public void StartRace()
{
RaceStartTime = Time.time;
RaceStarted = true;
finishedCount = 0;
UpdateStatus("スタート! 100メートル競争");
}
public void NotifyFinished(Runner r)
{
finishedCount++;
if (finishedCount == runners.Count)
{
RaceStarted = false;
ShowResults();
}
}
void Update()
{
// Escapeで終了
if (Input.GetKeyDown(KeyCode.Escape))
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
return;
}
if (!RaceStarted) return;
var last = GetLastPlace();
var first = GetFirstPlace();
if (last != null && first != null)
{
UpdateStatus($"先頭: {first.runnerName} 最下位: {last.runnerName}");
}
}
Runner GetLastPlace() => runners == null || runners.Count == 0 ? null : runners.OrderBy(r => r.Progress).First();
Runner GetFirstPlace() => runners == null || runners.Count == 0 ? null : runners.OrderByDescending(r => r.Progress).First();
void ShowResults()
{
var ordered = runners.OrderBy(r => r.finishTime).ToList();
string msg = "結果\n";
for (int i = 0; i < ordered.Count; i++)
{
var r = ordered[i];
msg += $"{i + 1}位: {r.runnerName} タイム: {r.finishTime:0.00}s\n";
}
UpdateStatus(msg);
Debug.Log(msg);
}
void UpdateStatus(string msg)
{
if (statusText != null) statusText.text = msg;
}
}
FollowCameraController.cs(抜粋)
[RequireComponent(typeof(Camera))]
public class FollowCameraController : MonoBehaviour
{
public float smoothTime = 0.25f; public float minDistance = 15f; public float extraMargin = 3f; public float heightFactor = 0.6f;
void LateUpdate()
{
var runners = FindObjectsOfType<Runner>(); if (runners.Length == 0) return;
Bounds b = new Bounds(runners[0].transform.position, Vector3.zero);
foreach (var r in runners) b.Encapsulate(r.transform.position); b.Expand(extraMargin * 2f);
var last = runners.OrderBy(r => r.Progress).First();
float radius = Mathf.Max(b.extents.x, b.extents.z);
float dist = Mathf.Max(minDistance, radius / Mathf.Tan(GetComponent<Camera>().fieldOfView * Mathf.Deg2Rad * 0.5f) + extraMargin);
Vector3 desired = new Vector3(b.center.x, b.center.y + Mathf.Max(5f, radius * heightFactor), last.transform.position.z - dist);
transform.position = Vector3.SmoothDamp(transform.position, desired, ref _v, smoothTime);
transform.LookAt(last.transform.position);
}
Camera _c; Vector3 _v; void Awake(){ _c = GetComponent<Camera>(); }
}
-
エディタメソッド
Assets/Editor/SetupRun100Scene.csを用意し、-executeMethod Run100SceneSetup.Setupで実行。 -
初期エラー:
UnityEngine.UI参照不足 →Packages/manifest.jsonに"com.unity.ugui": "1.0.0"を追記し解決。 -
文字フォント:
Arial.ttfがビルトイン非推奨 →LegacyRuntime.ttfに置換して解決。 -
仕様変更(地面が途中で切れる)→ 地面をZ=50中心・スケール(2,1,15)に拡張。再実行時に既存生成物(Ground/Runner/UI等)をクリーンアップ。
-
実行コマンド
"...\Unity.exe" -batchmode -nographics -projectPath "...\TestCodex" `
-executeMethod Run100SceneSetup.Setup -quit -logFile "...\SetupRun100Scene.log"
5. Windowsビルドの自動化
-
Assets/Editor/BuildWindows.csを追加。run100シーンのみをビルド。 -
出力先:
TestCodex/Run_yyyyMMdd_HHmmss/TestCodex.exe -
一度、プロジェクト外に出力したが、指示に従い
TestCodex直下に修正。 -
文字列置換の不具合で一時的にC#構文エラーになったため、スクリプトをクリーンな全文で置換して解消。
-
実行コマンド
"...\Unity.exe" -batchmode -nographics -projectPath "...\TestCodex" `
-executeMethod BuildWindows.Run -quit -logFile "...\BuildWindows.log"
- 生成例
-
TestCodex\Run_20251111_105656\TestCodex.exe(初回: プロジェクト外に出力していた版) -
TestCodex\Run_20251111_110133\TestCodex.exe(出力先をプロジェクト直下に修正) -
TestCodex\Run_20251111_110416\TestCodex.exe(地面修正反映後) -
TestCodex\Run_20251111_110714\TestCodex.exe(Escape終了対応後)
-
6. 最終確認と操作
- シーン:
Assets/Scenes/run100.unity - 実行: いずれかの
Run_*フォルダ内TestCodex.exe - 操作: ゲーム中に
Escapeキーで終了(エディタでは再生停止)
付録: SetupRun100Scene.cs(主要部・全文)
using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI;
public static class Run100SceneSetup
{
public static void Setup()
{
var scenePath = "Assets/Scenes/run100.unity";
var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
// 既存生成物をクリーンアップ
foreach (var go in scene.GetRootGameObjects())
{
if (go.name == "Ground" || go.name == "RaceManager" || go.name == "Canvas" || go.name == "StatusText" || go.GetComponent<Runner>() != null)
Object.DestroyImmediate(go);
}
// 地面(0..100mを確実にカバー)
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
ground.name = "Ground"; ground.transform.position = new Vector3(0,0,50); ground.transform.localScale = new Vector3(2,1,15);
// ランナー
CreateRunner(PrimitiveType.Sphere, new Vector3(-2f, 0.5f, 0f), "Sphere");
CreateRunner(PrimitiveType.Cube, new Vector3( 0f, 0.5f, 0f), "Cube");
CreateRunner(PrimitiveType.Capsule, new Vector3( 2f, 1.0f, 0f), "Capsule");
// マネージャ
var managerGO = new GameObject("RaceManager"); var manager = managerGO.AddComponent<RaceManager>();
// UI
var canvasGO = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
var canvas = canvasGO.GetComponent<Canvas>(); canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = canvasGO.GetComponent<CanvasScaler>(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920,1080);
var textGO = new GameObject("StatusText", typeof(Text)); textGO.transform.SetParent(canvasGO.transform, false);
var text = textGO.GetComponent<Text>(); text.alignment = TextAnchor.UpperLeft; text.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); text.fontSize = 28; text.color = Color.white; text.text = "準備中";
var rt = text.GetComponent<RectTransform>(); rt.anchorMin = new Vector2(0,1); rt.anchorMax = new Vector2(0,1); rt.pivot = new Vector2(0,1); rt.anchoredPosition = new Vector2(20,-20); rt.sizeDelta = new Vector2(800,200);
typeof(RaceManager).GetField("statusText", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(manager, text);
// カメラ
Camera cam = Object.FindObjectOfType<Camera>(); if (cam == null) cam = new GameObject("Main Camera", typeof(Camera)).GetComponent<Camera>(); cam.tag = "MainCamera";
cam.transform.position = new Vector3(0,12,-20); cam.transform.LookAt(new Vector3(0,0,0)); if (cam.gameObject.GetComponent<FollowCameraController>() == null) cam.gameObject.AddComponent<FollowCameraController>();
EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); AssetDatabase.Refresh();
}
static void CreateRunner(PrimitiveType type, Vector3 position, string name)
{
var go = GameObject.CreatePrimitive(type); go.name = name; go.transform.position = position;
var r = go.AddComponent<Runner>(); r.runnerName = name;
var renderer = go.GetComponent<Renderer>(); if (renderer != null) { var mat = new Material(Shader.Find("Standard")); mat.color = name=="Sphere"? Color.red : name=="Cube"? Color.green : Color.blue; renderer.sharedMaterial = mat; }
}
}
付録: BuildWindows.cs(最終版・全文)
using System; using System.IO; using UnityEditor; using UnityEditor.Build.Reporting; using UnityEditor.SceneManagement; using UnityEngine;
public static class BuildWindows
{
public static void Run()
{
string scenePath = "Assets/Scenes/run100.unity";
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows64);
PlayerSettings.SetScriptingBackend(BuildTargetGroup.Standalone, ScriptingImplementation.Mono2x);
string projectRoot = Directory.GetParent(Application.dataPath).FullName; string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string outDir = Path.Combine(projectRoot, $"Run_{timestamp}"); Directory.CreateDirectory(outDir);
string exePath = Path.Combine(outDir, "TestCodex.exe");
var options = new BuildPlayerOptions { scenes = new[] { scenePath }, locationPathName = exePath, target = BuildTarget.StandaloneWindows64, options = BuildOptions.None };
BuildReport report = BuildPipeline.BuildPlayer(options); if (report.summary.result != BuildResult.Succeeded) throw new Exception($"Build failed: {report.summary.result} - {report.summary.totalErrors} errors"); Debug.Log($"Build succeeded: {exePath}");
}
}
参考ログ・ファイル
- 作成ログ:
C:\Users\user\Desktop\test\TestCodex.create.log - シーン作成:
C:\Users\user\Desktop\test\CreateRun100Scene.log - シーンセットアップ:
C:\Users\user\Desktop\test\SetupRun100Scene.log - ビルドログ:
C:\Users\user\Desktop\test\BuildWindows.log - 生成EXE例:
...\TestCodex\Run_20251111_110133\TestCodex.exe...\TestCodex\Run_20251111_110416\TestCodex.exe...\TestCodex\Run_20251111_110714\TestCodex.exe
まとめ
- 一番の驚き: Unityエディタを一度も起動せず、プロンプトとCLIだけでプロジェクト作成・シーン構築・ビルド・スクリーンショットまで完了しました。再現性が高く、CI/CDや自動化に直結します。
- Editor CLI+エディタスクリプトで「シーン構築」と「ビルド」を自動化。
- 実装・改善サイクル(UI参照不足、フォント置換、地面長さ、Escape終了)をログで可視化。
- 小規模プロトタイプでも、手戻り少なく再現性のある環境を作れるのが利点です。
追記
プロンプトになんと入力したのって言われたので書き出します。
- ここにunityを使って新規にプロジェクトを作ってプロジェクトの名前はTestCodex
- このプロジェクトは100メール競争をするアプリにしたい。まずはrun100シーンを作って
- スフィアとキューブとカプセルの3dオブジェクトをおいてスタートしたら100メールを移動して勝敗を決めて、カメラは常に最下位のオブジェクトに向くが全体を見れるように動いてくれ
- 現状を確認したいから一度windows向けにビルドしてくれフォルダはバージョンが分かるようにRun+日時でつくって
- TestCodex以下にビルド作って。
- 100メール用の地面が途中で切れちゃってるね。
- エスケープキーでアプリ終了するようにして
これだけです。
Codexのログは
C:\Users\uesr\.codex\sessions\ 以下に全部保存されています。

