feat(core): implement playable CTF prototype with 6 core scripts
Implements the minimal playable core for the teaser prototype: - Game.cs: Bootstrap, scene setup, scoring, round reset, win condition - Unit.cs: Movement, route following, tagging, respawn - RouteDrawer.cs: Click-drag route input with LineRenderer preview - Flag.cs: Pickup, drop, return mechanics - Visibility.cs: Fog of war via SpriteRenderer visibility - SimpleAI.cs: Enemy AI that chases player flag All game objects created programmatically (no Editor setup required). Uses RuntimeInitializeOnLoadMethod for automatic bootstrap. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
231
Backyard CTF/Assets/Scripts/Unit.cs
Normal file
231
Backyard CTF/Assets/Scripts/Unit.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class Unit : MonoBehaviour
|
||||
{
|
||||
public enum Team { Player, Enemy }
|
||||
|
||||
public Team team;
|
||||
public bool isTaggedOut;
|
||||
public bool hasFlag;
|
||||
|
||||
List<Vector2> route = new();
|
||||
int routeIndex;
|
||||
bool isMoving;
|
||||
|
||||
SpriteRenderer spriteRenderer;
|
||||
CircleCollider2D circleCollider;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
circleCollider = GetComponent<CircleCollider2D>();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (isTaggedOut || !isMoving || route.Count == 0) return;
|
||||
|
||||
FollowRoute();
|
||||
}
|
||||
|
||||
void FollowRoute()
|
||||
{
|
||||
if (routeIndex >= route.Count)
|
||||
{
|
||||
isMoving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 target = route[routeIndex];
|
||||
Vector2 current = transform.position;
|
||||
Vector2 direction = (target - current).normalized;
|
||||
float distance = Vector2.Distance(current, target);
|
||||
|
||||
// Check for obstacles ahead
|
||||
if (IsObstacleAhead(direction))
|
||||
{
|
||||
isMoving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
float moveDistance = Game.UnitSpeed * Time.deltaTime;
|
||||
|
||||
if (moveDistance >= distance)
|
||||
{
|
||||
// Reached waypoint
|
||||
transform.position = new Vector3(target.x, target.y, 0);
|
||||
routeIndex++;
|
||||
|
||||
if (routeIndex >= route.Count)
|
||||
{
|
||||
isMoving = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Move toward waypoint
|
||||
Vector2 newPos = current + direction * moveDistance;
|
||||
transform.position = new Vector3(newPos.x, newPos.y, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsObstacleAhead(Vector2 direction)
|
||||
{
|
||||
// Raycast to check for obstacles (non-trigger colliders)
|
||||
var hits = Physics2D.RaycastAll(transform.position, direction, 0.6f);
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
if (hit.collider != null && hit.collider.gameObject != gameObject && !hit.collider.isTrigger)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetRoute(List<Vector2> waypoints)
|
||||
{
|
||||
if (isTaggedOut) return;
|
||||
|
||||
route = new List<Vector2>(waypoints);
|
||||
routeIndex = 0;
|
||||
isMoving = route.Count > 0;
|
||||
}
|
||||
|
||||
public void ClearRoute()
|
||||
{
|
||||
route.Clear();
|
||||
routeIndex = 0;
|
||||
isMoving = false;
|
||||
}
|
||||
|
||||
void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (isTaggedOut) return;
|
||||
|
||||
// Check for enemy unit collision (tagging)
|
||||
var otherUnit = other.GetComponent<Unit>();
|
||||
if (otherUnit != null && otherUnit.team != team && !otherUnit.isTaggedOut)
|
||||
{
|
||||
HandleTagCollision(otherUnit);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for flag pickup
|
||||
var flag = other.GetComponent<Flag>();
|
||||
if (flag != null && flag.team != team && flag.carriedBy == null)
|
||||
{
|
||||
flag.Pickup(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for scoring (reaching own base with enemy flag)
|
||||
var baseZone = other.GetComponent<BaseZone>();
|
||||
if (baseZone != null && baseZone.team == team && hasFlag)
|
||||
{
|
||||
// Get the flag we're carrying
|
||||
var carriedFlag = team == Team.Player ? Game.Instance.enemyFlag : Game.Instance.playerFlag;
|
||||
if (carriedFlag != null && carriedFlag.carriedBy == this)
|
||||
{
|
||||
carriedFlag.Drop();
|
||||
Game.Instance.Score(team);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HandleTagCollision(Unit other)
|
||||
{
|
||||
// Determine who gets tagged: farther from their own base loses
|
||||
Transform myBase = Game.Instance.GetBase(team);
|
||||
Transform theirBase = Game.Instance.GetBase(other.team);
|
||||
|
||||
float myDistance = Vector2.Distance(transform.position, myBase.position);
|
||||
float theirDistance = Vector2.Distance(other.transform.position, theirBase.position);
|
||||
|
||||
if (myDistance > theirDistance)
|
||||
{
|
||||
TagOut();
|
||||
}
|
||||
else if (theirDistance > myDistance)
|
||||
{
|
||||
other.TagOut();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Equal distance - both get tagged (more chaos!)
|
||||
TagOut();
|
||||
other.TagOut();
|
||||
}
|
||||
}
|
||||
|
||||
public void TagOut()
|
||||
{
|
||||
if (isTaggedOut) return;
|
||||
|
||||
isTaggedOut = true;
|
||||
isMoving = false;
|
||||
route.Clear();
|
||||
|
||||
// Drop flag if carrying
|
||||
if (hasFlag)
|
||||
{
|
||||
var flag = team == Team.Player ? Game.Instance.enemyFlag : Game.Instance.playerFlag;
|
||||
if (flag != null && flag.carriedBy == this)
|
||||
{
|
||||
flag.Drop();
|
||||
}
|
||||
}
|
||||
|
||||
// Visual feedback - fade out
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
var color = spriteRenderer.color;
|
||||
color.a = 0.3f;
|
||||
spriteRenderer.color = color;
|
||||
}
|
||||
|
||||
StartCoroutine(RespawnCoroutine());
|
||||
}
|
||||
|
||||
IEnumerator RespawnCoroutine()
|
||||
{
|
||||
yield return new WaitForSeconds(Game.RespawnDelay);
|
||||
|
||||
// Respawn at base
|
||||
Transform baseTransform = Game.Instance.GetBase(team);
|
||||
Vector3 offset = new Vector3(Random.Range(-2f, 2f), team == Team.Player ? -2f : 2f, 0);
|
||||
transform.position = baseTransform.position + offset;
|
||||
|
||||
isTaggedOut = false;
|
||||
|
||||
// Restore visual
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
var color = spriteRenderer.color;
|
||||
color.a = 1f;
|
||||
spriteRenderer.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public void ForceRespawn(Vector3 position)
|
||||
{
|
||||
StopAllCoroutines();
|
||||
transform.position = position;
|
||||
isTaggedOut = false;
|
||||
isMoving = false;
|
||||
route.Clear();
|
||||
hasFlag = false;
|
||||
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
var color = spriteRenderer.color;
|
||||
color.a = 1f;
|
||||
spriteRenderer.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMoving => isMoving;
|
||||
public List<Vector2> CurrentRoute => route;
|
||||
}
|
||||
Reference in New Issue
Block a user