Really Basic rts mechanics.

Really need a way to attack and enemies to be a true MVP.
But for iteration 0 this is fine.
Need Kenney Art.
Need a way to Attack.
Need Rally Points.
Need Fog of War.
Need a HUD for better unit selection.
Need Mini Map.
I can go on, this is by far the most complex Prototype I've ever done.
This commit is contained in:
2021-02-15 15:41:17 -06:00
commit 2e068a625c
57 changed files with 7440 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
using UnityEngine;
// TODO: remove me!
public class AddToTeam : MonoBehaviour
{
public bool addToBuilding;
private void Start()
{
var building = GetComponent<Building>();
if (addToBuilding)
{
Team.AllBuildings.Add(building);
}
var unit = GetComponent<Unit>();
if (!addToBuilding)
{
Team.AllUnits.Add(unit);
}
Destroy(this);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 51bf038dd0e34e240b57160e0b947f93
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+130
View File
@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
// TODO: The actual building logic and the construction logic should be separated!
[RequireComponent(typeof(BoxCollider2D), typeof(SpriteRenderer))]
public class Building : MonoBehaviour
{
private struct WorkOrder
{
public float startTime;
public float duration;
public Action completed;
}
[Serializable]
public struct UnitCommand
{
public Unit unit;
public Key keyControl;
public int cost;
public float buildDuration;
}
[NonSerialized] public BoxCollider2D buildingCollider;
public int maxQueueCount;
public float constructionTime;
public bool startBuilt;
public int buildingCost;
public List<UnitCommand> buildableUnits;
[NonSerialized] public bool isUnderConstruction = true;
private readonly Queue<WorkOrder> _workQueue = new Queue<WorkOrder>();
private WorkOrder _currentWorkOrder;
private bool _hasCurrentWorkOrder;
// In seconds.
private float _currentConstructionProgress;
private readonly Color _constructionColor = Color.black;
private SpriteRenderer _sprite;
private Transform _transform;
private void Start()
{
buildingCollider = GetComponent<BoxCollider2D>();
_sprite = GetComponent<SpriteRenderer>();
isUnderConstruction = !startBuilt;
_transform = transform;
}
public void UpdateBuildProgress(float amount)
{
if (isUnderConstruction)
{
_currentConstructionProgress += amount;
if (_currentConstructionProgress >= constructionTime)
{
isUnderConstruction = false;
}
_sprite.color = Color.Lerp(_constructionColor, Color.white,
_currentConstructionProgress / constructionTime);
}
}
private void Update()
{
if (_hasCurrentWorkOrder)
{
if (Time.time > _currentWorkOrder.startTime + _currentWorkOrder.duration)
{
_currentWorkOrder.completed();
_hasCurrentWorkOrder = false;
}
}
else
{
// Could probably do all this once we complete the order.
// Would also need to do it when we receive an order.
if (_workQueue.Count > 0)
{
_currentWorkOrder = _workQueue.Dequeue();
_currentWorkOrder.startTime = Time.time;
_hasCurrentWorkOrder = true;
}
}
}
public void ProcessKeyInput(Key key)
{
if (isUnderConstruction)
{
return;
}
foreach (var command in buildableUnits)
{
if (command.keyControl == key)
{
if (command.cost > Team.Minerals)
{
// TODO: announcer.
Debug.LogWarning("You've not enough minerals");
break;
}
if (maxQueueCount > _workQueue.Count)
{
Team.Minerals -= command.cost;
_workQueue.Enqueue(new WorkOrder
{
startTime = Time.time,
duration = command.buildDuration,
completed = () =>
{
var unitGo = Instantiate(command.unit,
_transform.position, _transform.rotation);
Team.AllUnits.Add(unitGo);
}
});
}
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7dcd2279d5fa112459fa847e09209f5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+275
View File
@@ -0,0 +1,275 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Camera))]
public class CameraController : MonoBehaviour
{
public float scrollSpeed;
public RectTransform unitSelector;
// TODO: this is not ideal.
public List<Minerals> minerals;
private Camera _camera;
private Keyboard _keyboard;
private Mouse _mouse;
private Transform _transform;
private Vector3 _positionChange;
private bool _isSelecting;
private Rect _selectorRect;
private readonly List<Unit> _selectedUnits = new List<Unit>();
private Building _currentBuilding;
private bool _wantsBuildBarracks;
private void Start()
{
_mouse = Mouse.current;
_keyboard = Keyboard.current;
_transform = transform;
_positionChange = Vector3.zero;
_camera = GetComponent<Camera>();
Cursor.lockState = CursorLockMode.Confined;
var pivot = unitSelector.pivot;
pivot.x = 0;
pivot.y = 0;
unitSelector.pivot = pivot;
Team.Minerals = 10;
_keyboard.onTextInput += OnTextInput;
}
private void OnTextInput(char ch)
{
var keyControl = _keyboard.FindKeyOnCurrentKeyboardLayout(ch.ToString());
if (_currentBuilding != null && keyControl != null)
{
_currentBuilding.ProcessKeyInput(keyControl.keyCode);
}
}
private void Update()
{
#if UNITY_EDITOR
if (_keyboard.escapeKey.wasPressedThisFrame)
{
Cursor.lockState = CursorLockMode.None;
}
#endif
if (!Application.isFocused)
{
return;
}
var mousePos = _mouse.position.ReadValue();
_positionChange.x = 0;
_positionChange.y = 0;
if (mousePos.x <= 1)
{
_positionChange.x -= scrollSpeed * Time.deltaTime;
}
if (mousePos.x >= Screen.width - 1)
{
_positionChange.x += scrollSpeed * Time.deltaTime;
}
if (mousePos.y <= 1)
{
_positionChange.y -= scrollSpeed * Time.deltaTime;
}
if (mousePos.y >= Screen.height - 1)
{
_positionChange.y += scrollSpeed * Time.deltaTime;
}
_transform.position += _positionChange;
if (_mouse.rightButton.wasPressedThisFrame)
{
// TODO: pick logic, if the mouse was not dragged then it should select whatever is in the tile.
var worldPos = _camera.ScreenToWorldPoint(mousePos);
worldPos.x = Mathf.RoundToInt(worldPos.x);
worldPos.y = Mathf.RoundToInt(worldPos.y);
worldPos.z = 0;
var hasIssuedCommand = false;
foreach (var building in Team.AllBuildings)
{
if (building.buildingCollider.OverlapPoint(worldPos) && building.isUnderConstruction)
{
//var worker = Team.AllUnits.FirstOrDefault() as Worker;
var worker = _selectedUnits.FirstOrDefault(u => u is Worker) as Worker;
if (worker)
{
hasIssuedCommand = true;
worker.ConstructBuilding(building, worldPos);
}
break;
}
}
foreach (var mineral in minerals)
{
if (mineral.boxCollider.OverlapPoint(worldPos))
{
var cmd = Team.AllBuildings.FirstOrDefault(b => b.name == "CommandCenter");
// Not the best, but it will do.
// TODO: basically will need to search all buildings for command centers (should be a type?) then find the closest to the patch.
if (cmd)
{
var workers = _selectedUnits.FindAll(u => u is Worker);
hasIssuedCommand = workers.Count > 0;
Debug.Log($"has issued cmd: {hasIssuedCommand}");
workers.ForEach(w => ((Worker)w).StartMining(mineral,
mineral.transform.position,
cmd.transform.position));
}
}
}
if (!hasIssuedCommand)
{
MoveAllSelectedUnits(worldPos);
}
}
// TODO: for the selection box, we should select whatever thing there is most of in the box.
// IE: if there is one worker but 3 combat units, then select the 3 combat units.
// This is not ideal but since the prototype for iteration 0 is limited to 2 buildings, a worker, and a combat
// unit for the types of selectables, I think it will be acceptable until I add more stuff.
if (_mouse.leftButton.wasPressedThisFrame && !_wantsBuildBarracks)
{
_isSelecting = true;
_selectorRect = unitSelector.rect;
_selectorRect.min = _mouse.position.ReadValue();
}
if (_isSelecting)
{
_selectorRect.max = _mouse.position.ReadValue();
if (_mouse.leftButton.wasReleasedThisFrame)
{
// Needs to be called before we clear the selector rect.
FillSelection();
_isSelecting = false;
_selectorRect.min = Vector2.zero;
_selectorRect.max = Vector2.zero;
unitSelector.anchoredPosition = _selectorRect.min;
unitSelector.sizeDelta = _selectorRect.max;
}
Vector2 min;
min.x = _selectorRect.min.x > _selectorRect.max.x ? _selectorRect.max.x : _selectorRect.min.x;
min.y = _selectorRect.min.y > _selectorRect.max.y ? _selectorRect.max.y : _selectorRect.min.y;
Vector2 max;
max.x = _selectorRect.min.x > _selectorRect.max.x ? _selectorRect.min.x : _selectorRect.max.x;
max.y = _selectorRect.min.y > _selectorRect.max.y ? _selectorRect.min.y : _selectorRect.max.y;
unitSelector.anchoredPosition = min;
unitSelector.sizeDelta = max - min;
}
// if (_keyboard.sKey.wasPressedThisFrame && _currentBuilding)
// {
// _currentBuilding.BuildWorker();
// }
// TODO: this needs to cost money
// B->B build barracks
if (_selectedUnits.Count > 0)
{
if (_keyboard.bKey.wasPressedThisFrame)
{
_wantsBuildBarracks = true;
}
}
if (_wantsBuildBarracks && _selectedUnits.Count > 0)
{
if (_mouse.leftButton.wasPressedThisFrame)
{
var worldPos = _camera.ScreenToWorldPoint(mousePos);
worldPos.z = 0;
worldPos.x = Mathf.RoundToInt(worldPos.x);
worldPos.y = Mathf.RoundToInt(worldPos.y);
var worker = _selectedUnits.FirstOrDefault() as Worker;
if (worker)
{
worker.BuildBarracks(worldPos);
_wantsBuildBarracks = false;
}
}
}
else
{
_wantsBuildBarracks = false;
}
// Barracks -> A marine
// B->C command center.
}
private void FillSelection()
{
_selectedUnits.Clear();
var worldMin = _camera.ScreenToWorldPoint(_selectorRect.min);
var worldMax = _camera.ScreenToWorldPoint(_selectorRect.max);
var min = Vector3.zero;
min.x = worldMin.x > worldMax.x ? worldMax.x : worldMin.x;
min.y = worldMin.y > worldMax.y ? worldMax.y : worldMin.y;
var max = Vector3.zero;
max.x = worldMin.x > worldMax.x ? worldMin.x : worldMax.x;
max.y = worldMin.y > worldMax.y ? worldMin.y : worldMax.y;
var hasUnits = false;
foreach (var unit in Team.AllUnits)
{
var pos = unit.transform.position;
if (pos.x > min.x && pos.y > min.y &&
pos.x < max.x && pos.y < max.y)
{
_selectedUnits.Add(unit);
hasUnits = true;
}
}
// TODO: this is really not good, Ideally would just have a selectable type that provides abilities to the user.
// IE a unit selectable type provides the ability to move. A building selectable type provides the ability to manufacture a unit. etc.
if (!hasUnits)
{
// Try to select a building
foreach (var building in Team.AllBuildings)
{
if (building.buildingCollider.OverlapPoint(min)
|| building.buildingCollider.OverlapPoint(max))
{
Debug.Log("Building selected");
_currentBuilding = building;
break;
}
}
}
}
private void MoveAllSelectedUnits(Vector3 worldPos)
{
// TODO: not sure if pathfinding should happen here or on each unit.
// Probably start with each unit and when swarming logic is needed re-think the solution.
foreach (var unit in _selectedUnits)
{
unit.MoveTo(worldPos);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5822fae2646a8c943b31d77e917f8d21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+20
View File
@@ -0,0 +1,20 @@
public class Marine : Unit
{
protected override void Start()
{
base.Start();
// TODO: obviously this will be removed...
MoveTo(_transform.position);
}
protected override void CancelCurrentTask()
{
}
// Update is called once per frame
private void Update()
{
MovementRoutine();
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c3931f60c368434f8e1375690a83af5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+19
View File
@@ -0,0 +1,19 @@
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Text))]
public class MineralDisplay : MonoBehaviour
{
private Text _text;
private void Start()
{
_text = GetComponent<Text>();
}
private void Update()
{
// TODO: probably want more of an event driven UI update.
_text.text = $"Minerals: {Team.Minerals}";
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eb5c2f049d9f7bb49b6570fed958ea9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -0,0 +1,15 @@
using System;
using UnityEngine;
[RequireComponent(typeof(BoxCollider2D))]
public class Minerals : MonoBehaviour
{
[NonSerialized]
public BoxCollider2D boxCollider;
public int amount;
private void Start()
{
boxCollider = GetComponent<BoxCollider2D>();
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 78d69b3c81ba1e54cbf6735fb66af605
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+18
View File
@@ -0,0 +1,18 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Pathfinder : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e98790d9791e90c498ee2b0187d66f76
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -0,0 +1,9 @@
using System.Collections.Generic;
// TODO: this is probably the least final piece of code in this project.
public static class Team
{
public static List<Unit> AllUnits = new List<Unit>();
public static List<Building> AllBuildings = new List<Building>();
public static int Minerals;
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c3744dac5e4f019449ee54cb770831b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+44
View File
@@ -0,0 +1,44 @@
using System;
using UnityEngine;
public abstract class Unit : MonoBehaviour
{
// Meters is 1 unity unit, also a tile width/height.
[Tooltip("How many meters per second")]
public float speed;
protected Transform _transform;
private Vector3 _startPosition;
private Vector3 _moveToPosition;
private float _startTime;
private float _moveDuration;
protected Action _doThingAfterMove;
protected virtual void Start()
{
_transform = transform;
}
protected void MovementRoutine()
{
_startTime += Time.deltaTime;
_transform.position = Vector3.Lerp(_startPosition, _moveToPosition, _startTime / _moveDuration);
if (_startTime / _moveDuration > 1.0f)
{
_doThingAfterMove?.Invoke();
}
}
protected abstract void CancelCurrentTask();
// TODO: replace this with pathfinding.
public virtual void MoveTo(Vector3 position)
{
CancelCurrentTask();
_startPosition = _transform.position;
_moveToPosition = position;
_moveDuration = Vector3.Distance(_startPosition, _moveToPosition) / speed;
_startTime = 0;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2719e163c057add49bd4bee7081826fd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+176
View File
@@ -0,0 +1,176 @@
using System;
using UnityEngine;
// TODO: maybe instead of inheritance I could use behaviours
// So the Unit will still be a thing, and that's what the CameraController interfaces with
// But then commands will be sent to any attached behaviour (worker, marine, etc).
// and that behaviour can do whatever it needs.
// Now that I think about it more, maybe more granular behaviours (move, mine, build, attack, etc).
public class Worker : Unit
{
private enum WorkerState
{
None,
Building,
Mining,
Moving
}
public Building barracksGo;
public float mineDuration;
public int mineralHarvestAmount;
private WorkerState _currentState;
private Building _constructionBuilding;
private Minerals _minerals;
private Vector2 _mineralPatch;
private Vector2 _commandCenter;
private int _currentMineralsHeld;
private float _mineStartTime;
protected override void Start()
{
base.Start();
_currentState = WorkerState.None;
}
private void Update()
{
// TODO: maybe a function pointer instead of this switch?
switch (_currentState)
{
case WorkerState.None:
// Literally do nothing.
break;
case WorkerState.Building:
BuildBarracks();
break;
case WorkerState.Mining:
MineMinerals();
break;
case WorkerState.Moving:
MovementRoutine();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void MineMinerals()
{
_currentState = WorkerState.Mining;
if (_currentMineralsHeld <= 0)
{
if ((Vector2) _transform.position != _mineralPatch)
{
MoveTo(_mineralPatch);
_doThingAfterMove = () =>
{
_mineStartTime = Time.time;
MineMinerals();
};
}
else
{
// TODO: wait to mine minerals;
if (Time.time >= _mineStartTime + mineDuration)
{
var amount = mineralHarvestAmount;
if (_minerals.amount < amount)
{
amount = _minerals.amount;
}
_minerals.amount -= amount;
_currentMineralsHeld = amount;
}
}
}
else
{
if ((Vector2) _transform.position != _commandCenter)
{
MoveTo(_commandCenter);
_doThingAfterMove = MineMinerals;
}
else
{
Team.Minerals += _currentMineralsHeld;
_currentMineralsHeld = 0;
}
}
}
private void BuildBarracks()
{
// Since we are in the Building state, just assume we have a construction building.
if (_constructionBuilding.isUnderConstruction)
{
_constructionBuilding.UpdateBuildProgress(Time.deltaTime);
}
else
{
// change state
_currentState = WorkerState.None;
}
}
private void InstantiateBarracks()
{
if (barracksGo.buildingCost > Team.Minerals)
{
Debug.LogWarning("You've not enough minerals!");
return;
}
Team.Minerals -= barracksGo.buildingCost;
_constructionBuilding = Instantiate(barracksGo,
_transform.position + new Vector3(0.5f, 0, 0),
_transform.rotation);
Team.AllBuildings.Add(_constructionBuilding);
_currentState = WorkerState.Building;
_doThingAfterMove = null;
}
protected override void CancelCurrentTask()
{
_currentState = WorkerState.None;
_doThingAfterMove = null;
// Might be able to just set the building to null all the time?
if (_currentState == WorkerState.Building)
{
_constructionBuilding = null;
}
}
public override void MoveTo(Vector3 position)
{
base.MoveTo(position);
_currentState = WorkerState.Moving;
}
// Called when the user selects the build barracks command.
public void BuildBarracks(Vector3 position)
{
MoveTo(position);
_doThingAfterMove = InstantiateBarracks;
}
// Called when the user right clicks on an unfinished building.
public void ConstructBuilding(Building building, Vector3 position)
{
MoveTo(position);
_constructionBuilding = building;
_doThingAfterMove = () => _currentState = WorkerState.Building;
}
public void StartMining(Minerals patch, Vector2 patchLocation, Vector2 commandCenterLocation)
{
_mineralPatch = patchLocation;
_commandCenter = commandCenterLocation;
_minerals = patch;
_currentState = WorkerState.Mining;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32f92ee571b07704d883772d5a8bb0d6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: