Files
Backyard-CTF/Backyard CTF/Assets/Scripts/Game.cs
John Lamb c4b3507272 fix(map): landscape orientation with left/right bases
- Map now 38x18 (19:9 landscape aspect ratio)
- Player base on left, enemy base on right
- Houses repositioned for horizontal play lanes
- Unit spawn offsets adjusted for horizontal layout

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 21:37:05 -06:00

466 lines
14 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class Game : MonoBehaviour
{
// Auto-bootstrap when game starts - no Editor setup needed
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Bootstrap()
{
if (Instance != null) return;
var gameGO = new GameObject("Game");
gameGO.AddComponent<Game>();
}
// ===========================================
// CONFIGURATION - All tunable constants here
// ===========================================
// Gameplay
public const int UnitsPerTeam = 3;
public const float UnitSpeed = 5f;
public const float UnitSize = 1f;
public const float UnitColliderRadius = 0.5f;
// Vision & Detection
public const float VisionRadius = 4f;
public const float TagRadius = 0.75f;
public const float FlagPickupRadius = 0.75f;
// Timing
public const float RespawnDelay = 5f;
public const float FlagReturnDelay = 5f;
// Scoring
public const int WinScore = 3;
// Map - 19:9 aspect ratio for smartphone (landscape)
public const float MapWidth = 38f;
public const float MapHeight = 18f;
// Bases
public const float BaseSize = 5f;
public const float BaseInset = 4f; // Distance from map edge
// Houses
public const float HouseMinSize = 2f;
public const float HouseMaxSize = 4f;
// References set during setup
public static Game Instance { get; private set; }
public List<Unit> playerUnits = new();
public List<Unit> enemyUnits = new();
public Transform playerBase;
public Transform enemyBase;
public Flag playerFlag;
public Flag enemyFlag;
int playerScore;
int enemyScore;
bool gameOver;
TextMeshProUGUI scoreText;
TextMeshProUGUI gameOverText;
void Awake()
{
Instance = this;
}
void Start()
{
SetupCamera();
CreateGround();
CreateObstacles();
CreateBases();
CreateFlags();
SpawnUnits();
CreateUI();
SetupSystems();
}
void SetupCamera()
{
var cam = Camera.main;
if (cam != null)
{
cam.orthographic = true;
cam.orthographicSize = MapHeight / 2f + 2f;
cam.transform.position = new Vector3(0, 0, -10);
}
}
void CreateGround()
{
var ground = CreateSprite("Ground", new Color(0.2f, 0.4f, 0.2f), MapWidth, MapHeight);
ground.transform.position = Vector3.zero;
var sr = ground.GetComponent<SpriteRenderer>();
sr.sortingOrder = -10;
}
void CreateObstacles()
{
// Dense house layout for landscape 38x18 map
// Bases on left/right, houses create horizontal lanes with cross-routes
Vector2[] housePositions = {
// Left section (near player base) - create 3 exit lanes
new(-14f, 5f),
new(-12f, -4f),
new(-10f, 0f),
// Left-mid section - force route decisions
new(-6f, 6f),
new(-4f, 2f),
new(-7f, -3f),
new(-3f, -5f),
// Center section - dense, creates cat-and-mouse area
new(2f, 5f),
new(0f, 1f),
new(-2f, -2f),
new(3f, -4f),
new(-1f, -6f),
new(1f, 6f),
// Right-mid section - mirror complexity
new(6f, 5f),
new(4f, -2f),
new(7f, -5f),
new(5f, 2f),
// Right section (near enemy base) - create 3 approach lanes
new(14f, 4f),
new(12f, -3f),
new(10f, 0f),
};
Vector2[] houseSizes = {
// Left section
new(2.5f, 3f),
new(3f, 2.5f),
new(2f, 2f),
// Left-mid
new(3f, 2.5f),
new(2f, 3f),
new(2.5f, 2f),
new(2.5f, 2.5f),
// Center - varied sizes for interesting gaps
new(2f, 3f),
new(3f, 2f),
new(2f, 2.5f),
new(2.5f, 2f),
new(2f, 2f),
new(2.5f, 2.5f),
// Right-mid
new(2.5f, 2.5f),
new(3f, 2f),
new(2.5f, 3f),
new(2f, 2f),
// Right section
new(3f, 2.5f),
new(2f, 3f),
new(2.5f, 2f),
};
for (int i = 0; i < housePositions.Length; i++)
{
var house = CreateSprite($"House_{i}", new Color(0.35f, 0.35f, 0.4f), houseSizes[i].x, houseSizes[i].y);
house.transform.position = new Vector3(housePositions[i].x, housePositions[i].y, 0);
var collider = house.AddComponent<BoxCollider2D>();
collider.size = Vector2.one; // Local space - scale handles actual size
var sr = house.GetComponent<SpriteRenderer>();
sr.sortingOrder = -5;
}
}
void CreateBases()
{
// Player base - left side
var playerBaseGO = CreateSprite("PlayerBase", new Color(0.2f, 0.3f, 0.8f, 0.5f), BaseSize, BaseSize);
playerBaseGO.transform.position = new Vector3(-MapWidth / 2f + BaseInset, 0, 0);
playerBase = playerBaseGO.transform;
var sr1 = playerBaseGO.GetComponent<SpriteRenderer>();
sr1.sortingOrder = -8;
var playerBaseTrigger = playerBaseGO.AddComponent<BoxCollider2D>();
playerBaseTrigger.isTrigger = true;
playerBaseTrigger.size = Vector2.one;
var playerBaseZone = playerBaseGO.AddComponent<BaseZone>();
playerBaseZone.team = Unit.Team.Player;
// Enemy base - right side
var enemyBaseGO = CreateSprite("EnemyBase", new Color(0.8f, 0.2f, 0.2f, 0.5f), BaseSize, BaseSize);
enemyBaseGO.transform.position = new Vector3(MapWidth / 2f - BaseInset, 0, 0);
enemyBase = enemyBaseGO.transform;
var sr2 = enemyBaseGO.GetComponent<SpriteRenderer>();
sr2.sortingOrder = -8;
var enemyBaseTrigger = enemyBaseGO.AddComponent<BoxCollider2D>();
enemyBaseTrigger.isTrigger = true;
enemyBaseTrigger.size = Vector2.one;
var enemyBaseZone = enemyBaseGO.AddComponent<BaseZone>();
enemyBaseZone.team = Unit.Team.Enemy;
}
void CreateFlags()
{
// Player flag at player base
var playerFlagGO = CreateSprite("PlayerFlag", new Color(0.3f, 0.5f, 1f), 1f, 1.5f);
playerFlagGO.transform.position = playerBase.position;
playerFlag = playerFlagGO.AddComponent<Flag>();
playerFlag.team = Unit.Team.Player;
playerFlag.homePosition = playerBase;
var flagCollider1 = playerFlagGO.AddComponent<CircleCollider2D>();
flagCollider1.isTrigger = true;
flagCollider1.radius = FlagPickupRadius;
var rb1 = playerFlagGO.AddComponent<Rigidbody2D>();
rb1.bodyType = RigidbodyType2D.Kinematic;
var sr1 = playerFlagGO.GetComponent<SpriteRenderer>();
sr1.sortingOrder = 5;
// Enemy flag at enemy base
var enemyFlagGO = CreateSprite("EnemyFlag", new Color(1f, 0.3f, 0.3f), 1f, 1.5f);
enemyFlagGO.transform.position = enemyBase.position;
enemyFlag = enemyFlagGO.AddComponent<Flag>();
enemyFlag.team = Unit.Team.Enemy;
enemyFlag.homePosition = enemyBase;
var flagCollider2 = enemyFlagGO.AddComponent<CircleCollider2D>();
flagCollider2.isTrigger = true;
flagCollider2.radius = FlagPickupRadius;
var rb2 = enemyFlagGO.AddComponent<Rigidbody2D>();
rb2.bodyType = RigidbodyType2D.Kinematic;
var sr2 = enemyFlagGO.GetComponent<SpriteRenderer>();
sr2.sortingOrder = 5;
}
void SpawnUnits()
{
// Spawn player units near player base
for (int i = 0; i < UnitsPerTeam; i++)
{
var offset = GetUnitSpawnOffset(i, UnitsPerTeam, isPlayer: true);
var unit = CreateUnit($"PlayerUnit_{i}", Unit.Team.Player, playerBase.position + offset);
playerUnits.Add(unit);
}
// Spawn enemy units near enemy base
for (int i = 0; i < UnitsPerTeam; i++)
{
var offset = GetUnitSpawnOffset(i, UnitsPerTeam, isPlayer: false);
var unit = CreateUnit($"EnemyUnit_{i}", Unit.Team.Enemy, enemyBase.position + offset);
enemyUnits.Add(unit);
}
}
Vector3 GetUnitSpawnOffset(int index, int total, bool isPlayer)
{
float spacing = 1.8f;
float yOffset = (index - (total - 1) / 2f) * spacing;
float xOffset = isPlayer ? 2f : -2f; // Offset toward center of map
return new Vector3(xOffset, yOffset, 0);
}
Unit CreateUnit(string name, Unit.Team team, Vector3 position)
{
Color color = team == Unit.Team.Player ? new Color(0.3f, 0.5f, 1f) : new Color(1f, 0.3f, 0.3f);
var unitGO = CreateSprite(name, color, UnitSize, UnitSize);
unitGO.transform.position = position;
var unit = unitGO.AddComponent<Unit>();
unit.team = team;
var collider = unitGO.AddComponent<CircleCollider2D>();
collider.isTrigger = true;
collider.radius = UnitColliderRadius;
var rb = unitGO.AddComponent<Rigidbody2D>();
rb.bodyType = RigidbodyType2D.Kinematic;
var sr = unitGO.GetComponent<SpriteRenderer>();
sr.sortingOrder = 10;
return unit;
}
void CreateUI()
{
// Create Canvas
var canvasGO = new GameObject("Canvas");
var canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGO.AddComponent<UnityEngine.UI.CanvasScaler>();
canvasGO.AddComponent<UnityEngine.UI.GraphicRaycaster>();
// Score text
var scoreGO = new GameObject("ScoreText");
scoreGO.transform.SetParent(canvasGO.transform, false);
scoreText = scoreGO.AddComponent<TextMeshProUGUI>();
scoreText.text = "0 - 0";
scoreText.fontSize = 48;
scoreText.alignment = TextAlignmentOptions.Center;
scoreText.color = Color.white;
var scoreRect = scoreGO.GetComponent<RectTransform>();
scoreRect.anchorMin = new Vector2(0.5f, 1f);
scoreRect.anchorMax = new Vector2(0.5f, 1f);
scoreRect.pivot = new Vector2(0.5f, 1f);
scoreRect.anchoredPosition = new Vector2(0, -20);
scoreRect.sizeDelta = new Vector2(200, 60);
// Game over text (hidden initially)
var gameOverGO = new GameObject("GameOverText");
gameOverGO.transform.SetParent(canvasGO.transform, false);
gameOverText = gameOverGO.AddComponent<TextMeshProUGUI>();
gameOverText.text = "";
gameOverText.fontSize = 72;
gameOverText.alignment = TextAlignmentOptions.Center;
gameOverText.color = Color.white;
gameOverText.gameObject.SetActive(false);
var gameOverRect = gameOverGO.GetComponent<RectTransform>();
gameOverRect.anchorMin = new Vector2(0.5f, 0.5f);
gameOverRect.anchorMax = new Vector2(0.5f, 0.5f);
gameOverRect.pivot = new Vector2(0.5f, 0.5f);
gameOverRect.anchoredPosition = Vector2.zero;
gameOverRect.sizeDelta = new Vector2(400, 100);
}
void SetupSystems()
{
// Add RouteDrawer
gameObject.AddComponent<RouteDrawer>();
// Add Visibility system
var visibility = gameObject.AddComponent<Visibility>();
visibility.playerUnits = playerUnits.ToArray();
visibility.enemyUnits = enemyUnits.ToArray();
visibility.enemyFlag = enemyFlag;
// Add SimpleAI
var ai = gameObject.AddComponent<SimpleAI>();
ai.aiUnits = enemyUnits.ToArray();
ai.playerFlag = playerFlag;
ai.aiBase = enemyBase;
}
public void Score(Unit.Team team)
{
if (gameOver) return;
if (team == Unit.Team.Player)
playerScore++;
else
enemyScore++;
UpdateScoreUI();
Debug.Log($"Score: {playerScore} - {enemyScore}");
if (playerScore >= WinScore)
EndGame(true);
else if (enemyScore >= WinScore)
EndGame(false);
else
ResetRound();
}
void UpdateScoreUI()
{
if (scoreText != null)
scoreText.text = $"{playerScore} - {enemyScore}";
}
void ResetRound()
{
// Return flags to home
playerFlag.ReturnHome();
enemyFlag.ReturnHome();
// Respawn all units at bases
for (int i = 0; i < playerUnits.Count; i++)
{
var offset = GetUnitSpawnOffset(i, playerUnits.Count, isPlayer: true);
playerUnits[i].ForceRespawn(playerBase.position + offset);
}
for (int i = 0; i < enemyUnits.Count; i++)
{
var offset = GetUnitSpawnOffset(i, enemyUnits.Count, isPlayer: false);
enemyUnits[i].ForceRespawn(enemyBase.position + offset);
}
}
void EndGame(bool playerWon)
{
gameOver = true;
if (gameOverText != null)
{
gameOverText.text = playerWon ? "YOU WIN!" : "YOU LOSE!";
gameOverText.color = playerWon ? Color.green : Color.red;
gameOverText.gameObject.SetActive(true);
}
Debug.Log(playerWon ? "Player wins!" : "Enemy wins!");
}
public Transform GetBase(Unit.Team team)
{
return team == Unit.Team.Player ? playerBase : enemyBase;
}
public Flag GetFlag(Unit.Team team)
{
return team == Unit.Team.Player ? playerFlag : enemyFlag;
}
// Helper to create a colored sprite
GameObject CreateSprite(string name, Color color, float width, float height)
{
var go = new GameObject(name);
var sr = go.AddComponent<SpriteRenderer>();
sr.sprite = CreateRectSprite();
sr.color = color;
go.transform.localScale = new Vector3(width, height, 1);
return go;
}
// Create a simple white 1x1 sprite
static Sprite cachedSprite;
static Sprite CreateRectSprite()
{
if (cachedSprite != null) return cachedSprite;
var tex = new Texture2D(1, 1);
tex.SetPixel(0, 0, Color.white);
tex.Apply();
cachedSprite = Sprite.Create(tex, new Rect(0, 0, 1, 1), new Vector2(0.5f, 0.5f), 1);
return cachedSprite;
}
}
// Simple component to identify base zones
public class BaseZone : MonoBehaviour
{
public Unit.Team team;
}