Scripting Sample: ORTS

I prototyped the gameplay for my Orbital RTS project using Unity and C# scripting.

Below is a screenshot of my project in Unity. The scripts directories are shown on the right. I wrote all the scripts, controlling everything about the game’s behavior: ship movement, combat, camera controls, UI, etc.

 

Here is proof that my scripts work: a video of the finished gameplay prototype:

 

Here is the text of one of my scripts. It controls how ships parse their targets.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

// This is the base class for UnitAttackManager and UnitAssistManager scripts.
// This base class shouldn't be used by a unit directly, because it doesn't differentiate between friendly and hostile targets.
public class UnitTargetManager : MonoBehaviour {

  [System.Serializable]
  public class Stats
  {
    [Tooltip("If a potential target is detected (by a weapon's collision sphere trigger) by is not inside the weapon's attack range, does this unit automatically move towards the target? This setting should be false for most units, as we dont' want them to move around without an explicit player or AI command.")]
    public bool autoAttackOutsideWeaponsRange = false;
    [Tooltip("Does this unit check line of sight to its attack target? This check uses a raycast, so should be turned off when not necessary.")]
    public bool checkTerrainLOS = false;
    [Tooltip("The amount of ammo this unit has for its weapons. If this unit doesn't use ammo, set to -1")]
    public int ammoCapacity = -1;
  }
  public Stats stats;

  [System.Serializable]
  public class UI
  {
    [Tooltip("This should be set to the prefab button that's used to give this unit an Attack or Assist command. This button appears in the Unit Control Panel.")]
    public GameObject unitControl_CommandButton;
  }
  public UI ui;

  [System.Serializable]
  public class Dynamic
  {
    [Tooltip("The object that this unit will prioritize as a target. This variable is typically set by script when player interaction gives this unit an explicit target. This unit is capable of attacking/assisting targets in range without this variable being set.")]
    public GameObject mainTargetObject;
    [Tooltip("This is an enemy unit target of opportunity that just entered this unit's weapons range.")]
    public GameObject opportunityTargetGameObject;
    [Tooltip("Tracks when an enemy target was last in weapons range.")]
    public float targetInRangeTimestamp;
    [Tooltip("The amount of ammo that this unit currently has.")]
    public int currAmmo;
    [Tooltip("A list of weapons that this unit has.")]
    public List<Behavior_Weapon> weaponsList;
    [Tooltip("This is dynamically populated by the individual weapons behaviors under this unit. Whenever a unit is in range of a weapon behavior, that unit is added to this list.")]
    public List<GameObject> weaponTargets;
    [Tooltip("A list of units to remove from weaponTargets.")]
    public List<GameObject> weaponTargetsRemoveList;
    [Tooltip("If this is true, then there's at least 1 enemy within shooting range of at least 1 weapon.")]
    public bool isAnyEnemyInRange = false;
    [Tooltip("If this is true, then the target this unit was told to attack is in range of at least 1 weapon.")]
    public bool isMainTargetInRange = false;
  }
  public Dynamic dynamic;

  public delegate void OnEventAction();
  public event OnEventAction OnUpdateAmmoDisplay;

  // Stores the last time that a line-of-sight raycast was done.
  protected float terrainRaycastTime = 0f;
  protected bool hasLineOfSightToTarget = true;
  // Used for logic in looping through a list of enemies.
  protected GameObject bestTarget;

  // This is a reference to the shared movement scripts on this unit
  protected SharedUnitScripts sharedUnitScripts;

  public virtual void Awake()
  {
    sharedUnitScripts = gameObject.GetComponent<SharedUnitScripts>();
    dynamic.currAmmo = stats.ammoCapacity;
  }

  public virtual void Start()
  {
    // If this script has an Attack or Assist button for the Unit Control Panel, then add it to the list of buttons for this unit.
    if (ui.unitControl_CommandButton)
      sharedUnitScripts.unitAttributes.dynamic.buttonList.Add(ui.unitControl_CommandButton);
  }

  public virtual void Update()
  {
    UpdateTargetSettings();
  }

  public virtual void FixedUpdate()
  {
    ControlAction();
  }

  // Determine where this unit should face or attack, based on commands that this unit has been given.
  public virtual void ControlAction()
  {
    // Only control this unit's actions if the UnitMove script isn't present.
    // This control script hasn't been tested - it's intended for use with the first satellite turret unit.
    if (!sharedUnitScripts.unitMoveScript)
    {
      // If this unit's main target is in range, turn to face it.
      if (dynamic.mainTargetObject && dynamic.isMainTargetInRange)
      {
        if (rigidbody)
        FaceTarget(dynamic.mainTargetObject);
      }
      // This unit doesn't have a main target, so face the opportunity target.
      else if (!dynamic.mainTargetObject)
      {
        if (rigidbody)
        FaceTarget(dynamic.opportunityTargetGameObject);
      }
    }
  }

  // Update the settings that track which targets are in range.
  public virtual void UpdateTargetSettings()
  {
    // Before we perform the setting updates, let's remove any targets that are no longer valid.
    CleanUpTargetLists();

    if (dynamic.weaponTargets.Count == 0)
    {
      dynamic.isMainTargetInRange = false;
      dynamic.opportunityTargetGameObject = null;
    }
    else
    {
      if ((dynamic.mainTargetObject != null) && IsTargetInWeaponsRange(dynamic.mainTargetObject))
      {
        dynamic.isMainTargetInRange = true;
        dynamic.opportunityTargetGameObject = null;
        dynamic.targetInRangeTimestamp = Time.time;
      }
      else
      {
        dynamic.isMainTargetInRange = false;
        UpdateOpportunityTarget();
        if (dynamic.opportunityTargetGameObject != null)
        {
          dynamic.targetInRangeTimestamp = Time.time;
        }
      }
    }

    dynamic.isAnyEnemyInRange = ((dynamic.opportunityTargetGameObject != null) || dynamic.isMainTargetInRange);
  }

  // Remove any units from the weaponTargets list that was marked for removal, and clear out the main target if it's no longer valid.
  public virtual void CleanUpTargetLists()
  {
    // Check to see if the main target is still valid. If not, then clear out this unit's main target setting.
    CleanUpMainTarget();

    // Remove targets from weaponTargets that are no longer valid.
    if (dynamic.weaponTargetsRemoveList.Count != 0)
    {
      foreach (GameObject unitToRemove in dynamic.weaponTargetsRemoveList)
      {
        dynamic.weaponTargets.Remove(unitToRemove);
      }
      dynamic.weaponTargetsRemoveList.Clear();
    }
  }

  // Check to see if the main target is still valid. If not, then clear out this unit's main target setting.
  public virtual void CleanUpMainTarget()
  {
    if (dynamic.mainTargetObject)
    {
      if (!IsUnitValidTarget(dynamic.mainTargetObject))
      {
        // It's no longer a valid target, so remove it as a target.
        dynamic.weaponTargetsRemoveList.Add(dynamic.mainTargetObject);
        dynamic.mainTargetObject = null;
      }
    }
  }

  // Look for any targets that might have entered into range
  // Return true if an opportunity target was found.
  public virtual bool UpdateOpportunityTarget()
  {
    // Check to see if we've already have a previous opportunity target
    if (!dynamic.opportunityTargetGameObject)
    {
      // There's no previous opportunity target. Find one.
      dynamic.opportunityTargetGameObject = FindClosestTarget();
    }
    // Make sure our previous opportunity target is still in range.
    else if (!IsTargetInWeaponsRange(dynamic.opportunityTargetGameObject))
    {
      // Our previous opportunity target is no longer in range. Find a new one.
      dynamic.opportunityTargetGameObject = FindClosestTarget();
    }
    // The previous opportunity target is still in range. Double-check to make sure that target is still valid.
    else
    {
      if (!IsUnitValidTarget(dynamic.opportunityTargetGameObject))
      {
        // It's no longer a valid target, so mark it for removal.
        dynamic.weaponTargetsRemoveList.Add(dynamic.opportunityTargetGameObject);
        dynamic.opportunityTargetGameObject = null;
      }
    }
    return (dynamic.opportunityTargetGameObject != null);
  }

  // Whether this unit keeps track of ammo usage.
  public virtual bool IsTrackingAmmo()
  {
    return (stats.ammoCapacity != -1);
  }

  // This function should be called by whatever is going to refill the ammo on this unit
  public virtual void RefillAmmo()
  {
    dynamic.currAmmo = stats.ammoCapacity;
    foreach (Behavior_Weapon weaponBehavior in dynamic.weaponsList)
    {
      weaponBehavior.OnAmmoNotEmpty();
    }
    sharedUnitScripts.UpdateUnitControlButtonsIfSelected();
  }

  // Returns true if this unit has ammo left
  public virtual bool HasAmmo()
  {
    return ((stats.ammoCapacity == -1) || (dynamic.currAmmo > 0));
  }

  // Called by weapons as they use ammo
  public virtual void UseAmmo()
  {
    // If this unit is about to use its last ammo, then remove its main target. We don't want this unit to chase after an enemy that it can no longer effectively attack.
    // Some units, like missile launchers, have a backup weapon. But we still want those units to remove their main targets because it's annoying to have them suicide-run enemy units without their primary weapons.
    if (dynamic.currAmmo == 1)
    {
      dynamic.mainTargetObject = null;
      // Also clear out attack waypoints.
      if (sharedUnitScripts.unitMoveScript.IsAttackWaypoint(sharedUnitScripts.unitMoveScript.dynamic.moveTargetGameObject))
      {
        sharedUnitScripts.unitMoveScript.dynamic.moveTargetGameObject = null;
      }
    }

    dynamic.currAmmo--;
    // If this weapon is out of ammo, then run scripts to remove its targets from the attack manager.
    if (!HasAmmo())
    {
      RemoveAmmoWeaponsTargets();
    }
  }

  // When this unit runs out of ammo, remove the targets from its weapons that require ammo.
  public virtual void RemoveAmmoWeaponsTargets()
  {
    foreach (Behavior_Weapon weaponBehavior in dynamic.weaponsList)
    {
      weaponBehavior.OnAmmoEmpty();
    }
  }

  // Used to remove a unit from the weapons target list, if it's not in range of any weapon that could fire on it.
  public virtual void RemoveUnitIfNoWeaponsCanFire(GameObject targetUnit)
  {
    RemoveUnitIfNoWeaponsCanFire(targetUnit, null);
  }

  // Used to remove a unit from the weapons target list, if it's not in range of any weapon that could fire on it.
  // Pass in the parameter exceptionWeapon. If exceptionWeapon is the only weapon that can fire, then this function will still remove the unit.
  // Pass in null for exceptionWeapon if no exceptions are wanted.
  public virtual void RemoveUnitIfNoWeaponsCanFire(GameObject targetUnit, Behavior_Weapon exceptionWeapon)
  {
    if (!CanAnyWeaponFireOnUnit(targetUnit, exceptionWeapon))
      dynamic.weaponTargets.Remove(targetUnit);
  }

  // Checks to see if a unit is in range of any weapon.
  public virtual bool CanAnyWeaponFireOnUnit(GameObject targetUnit)
  {
    return CanAnyWeaponFireOnUnit(targetUnit, null);
  }

  // Checks to see if a unit is in range of any weapon.
  // Pass in the parameter exceptionWeapon. If exceptionWeapon is the only weapon that can fire, then this function will still return false.
  // Pass in null for exceptionWeapon if no exceptions are wanted.
  public virtual bool CanAnyWeaponFireOnUnit(GameObject targetUnit, Behavior_Weapon exceptionWeapon)
  {
    bool canFireOnUnit = false;
    foreach (Behavior_Weapon currWeapon in dynamic.weaponsList)
    {
      if (currWeapon != exceptionWeapon)
      {
        if (currWeapon.CanWeaponFireOnUnit(targetUnit))
        {
          canFireOnUnit = true;
          break;
        }
      }
    }
    return canFireOnUnit;
  }

  // Returns true if another unit is still a valid target of this unit.
  // Checks:
  // if the unit exists and is active in the world.
  // If the unit has UnitAttributes.
  // If the unit is alive.
  // If the unit is in the same system as this unit.
  public virtual bool IsUnitValidTarget(GameObject targetUnit)
  {
    return (SharedScripts.IsUnitValid(targetUnit) && SharedScripts.IsObjectInSameSystem(gameObject, targetUnit));
  }

  // Makes this unit try to face the closest enemy.
  public virtual GameObject FindClosestTarget()
  {
    // Go through the list of enemy units in range, and compare their distances.
    foreach (GameObject foundTarget in dynamic.weaponTargets)
    {
      // In case a unit was destroyed in the previous frame, check to make sure it still exists
      if (foundTarget)
      {
        if (IsUnitValidTarget(foundTarget))
        {
          if (bestTarget == null)
            bestTarget = foundTarget;
          else
          {
            if (Vector3.Distance(transform.position, foundTarget.transform.position) < Vector3.Distance(transform.position, bestTarget.transform.position))
            {
              bestTarget = foundTarget;
            }
          }
        }
        else
          dynamic.weaponTargetsRemoveList.Add (foundTarget);
      }
      else
      {
        // Since this targetEnemy no longer exists, mark it for removal from the weaponTargets list
        dynamic.weaponTargetsRemoveList.Add (foundTarget);
      }
    }

    // Now that we've found the enemy, double-check that it's in weapons range. This matters if this unit is marked to check line-of-slight against terrain.
    if (stats.autoAttackOutsideWeaponsRange || IsTargetInWeaponsRange(bestTarget))
      return bestTarget;
    else
      return null;
  }

  // Returns true if this unit has any weapons behaviors that's in range of shooting the target unit enemyUnit.
  // If this unit is set to check terrain line of sight, then perform a raycast to do so.
  public virtual bool IsTargetInWeaponsRange (GameObject targetUnit)
  {
    // In case other units have destroyed the target already, check for its existance.
    if (targetUnit && targetUnit.activeSelf)
    {
      // Commented out because weapons now support being able to detect enemy targets without being in firing range yet. So, being in the weaponTargets list no longer means that the target is in range.
      //bool isWeaponTargeted = dynamic.weaponTargets.Contains(targetUnit);

      bool isWeaponTargeted = false;

      // Go through each of this unit's weapons to see if each individual weapon thinks the targetUnit is in range.
      // Only the weapons know if they can fire. For example, weapons can have auto-attack turned off via the Unit Control Panel, but be explicitly told to attack this particular unit with a Unit Control button.
      if (!isWeaponTargeted)
      {
        foreach (Behavior_Weapon foundWeapon in dynamic.weaponsList)
        {
          if (foundWeapon.CanWeaponFireOnUnit(targetUnit))
          {
            isWeaponTargeted = true;
            break;
          }
        }
      }

      // Is this unit set to check terrain line of sight when considering if a unit is in weapons range?
      if (stats.checkTerrainLOS && isWeaponTargeted)
      {
        // Only check for buildings every so often to control performance.
        // PERFORMANCE CAN STILL BE IMPROVED BY MAKING THE DELAY TIME GLOBAL, RATHER THAN PER UNIT.
        if ((terrainRaycastTime + SharedScripts.terrainRaycastDelay) < Time.time)
        {
          // Store the timestamp of when we're doing this raycast.
          terrainRaycastTime = Time.time;
          // Checks to see if the target is a building.
          Vector3 targetPos;
          Building_Snap buildingScript = gameObject.GetComponent<Building_Snap>();
          if (buildingScript)
          {
            // Get the adjusted building position, because we want to shoot at the top of the building. If we shoot at the center, it might be embedded in the ground.
            targetPos = sharedUnitScripts.GetBuildingOffsetPosition(targetUnit);
          }
          else
          {
            // The target isn't a building, so just use the target's transform.
            targetPos = targetUnit.transform.position;
          }

          float offset = 0f;
          // We need to start the raycast from the weapon muzzle points of this ship.
          foreach(Behavior_Weapon foundWeapon in dynamic.weaponsList)
          {
            float foundWeaponOffset = foundWeapon.GetMuzzleFireTransform().localPosition.y;
            // Find the weapon with the lowest Y local coordinate
            if (offset == 0f)
            {
              offset = foundWeaponOffset;
            }
            else
            {
              if (foundWeaponOffset < offset)
              {
                offset = foundWeaponOffset;
              }
            }
          }
          // The Layermask allows us to differentiate between the planet in the building's system vs planets in other systems.
          LayerMask systemMask = (1 << LayerMask.NameToLayer(LayerMask.LayerToName(gameObject.layer)));
          // Start the raycast from the offset-down position of the weapon puzzle.
          if (SharedScripts.IsTerrainBetweenPositions((transform.position + (transform.up * offset)), targetPos, systemMask))
          {
            hasLineOfSightToTarget = false;
            return false;
          }
          else
          {
            hasLineOfSightToTarget = true;
            return true;
          }
        }
        else
        {
          return hasLineOfSightToTarget;
        }
      }
      // This unit is set to not check line-of-sight, so just return whether the target is in range of a weapon.
      else return isWeaponTargeted;
    }
    // The enemy unit is no longer valid, so return false.
    else
      return false;
  }

  // Turn to face the target currTarget
  // This control script hasn't been tested - it's intended for use with the first satellite turret unit.
  public virtual void FaceTarget (GameObject currTarget)
  {
    // Before we start checking for facing targets, check to see if this unit has the movement script
    if (sharedUnitScripts.unitMoveScript)
    {
      // If this unit doesn't have a move target, facing target, or attack target, then make it turn towards its opportunity target.
      if (!sharedUnitScripts.unitMoveScript.dynamic.moveTargetGameObject && !sharedUnitScripts.unitMoveScript.dynamic.facingTargetGameObject)
      {
        sharedUnitScripts.FaceToTarget(currTarget, sharedUnitScripts.unitMoveScript.movement.rotationSpeed, sharedUnitScripts.unitMoveScript.movement.maxBankAngle);
      }
    }
    // This is a stationary unit. Since we already know it doesn't have an attack target, make it face its opportunity target.
    else
    {
      // Until we have stationary rotation speed settings from the stationary unit attributes, pass in a default rotation speed setting.
      // Pass in 0 for the banking rotation value.
      sharedUnitScripts.FaceToTarget(currTarget, 2f, 0f);
    }
  }

  public virtual void EnableAllWeapons()
  {
    foreach (Behavior_Weapon foundWeapon in dynamic.weaponsList)
    {
      foundWeapon.dynamic.isWeaponActive = true;
    }
  }

  public virtual void DisableAllWeapons()
  {
    foreach (Behavior_Weapon foundWeapon in dynamic.weaponsList)
    {
      foundWeapon.dynamic.isWeaponActive = false;
    }
  }

}