Skip to content

Instantly share code, notes, and snippets.

@addie-lombardo
Created April 28, 2022 16:18
Show Gist options
  • Save addie-lombardo/3b8b2fa4aecb3800e87e7139140f3cb4 to your computer and use it in GitHub Desktop.
Save addie-lombardo/3b8b2fa4aecb3800e87e7139140f3cb4 to your computer and use it in GitHub Desktop.
Space Capitalism Torch Aiming Code
using System;
using System.Collections;
using DG.Tweening;
using Fungus;
using Micosmo.SensorToolkit;
using RootMotion.FinalIK;
using UnityEngine;
using VLB;
namespace HumbleGrove.SpaceCapitalism
{
[RequireComponent(typeof(Animator), typeof(AimController))]
public class AimActor : SpaceCapitalismMonoBehaviour
{
#region Enums and Structs
public enum AimActorState
{
Uninitialized, // before the aim actor is initialized
Idle, // when the prominent player's torch is not aimed
IdleAiming, // when the non prominent player is providing ambient fill light
AimingAfterIdleAim, // when the newly prominent player's flashlight is briefly off after transitioning from IdleAiming
Aiming, // when the prominent player is aiming but not focusing on any selectables
Focusing // when the prominent player is aiming at a selectable
}
[Serializable]
public struct TorchProperties
{
[Range(1, 180)]
public float ColliderAngles;
[Min(0f)]
public float InnerSpotAngle;
[Min(0f)]
public float OuterSpotAngle; // vlb spot angle is the same as this
[Min(0f)]
public float SpotlightIntensity;
[Min(0f)]
public float VlbIntensity;
public bool VlbNoiseEnabled;
[Range(0, 1)]
public float ParticlesAlpha;
[Min(0f)]
public float ParticlesDensity;
[MinMaxRange(0.0f, 1.0f)]
public MinMaxRangeFloat ParticlesDistanceRange;
public static readonly TorchProperties IdleDefaults = new()
{
ColliderAngles = 20f,
InnerSpotAngle = 21.80208f,
OuterSpotAngle = 30f,
SpotlightIntensity = 3f,
VlbIntensity = 0.03f,
VlbNoiseEnabled = true,
ParticlesAlpha = 0.1f,
ParticlesDensity = 2,
ParticlesDistanceRange = new MinMaxRangeFloat(0f, 0.3f)
};
public static readonly TorchProperties IdleAimingDefaults = new()
{
ColliderAngles = 20f,
InnerSpotAngle = 120.5997f,
OuterSpotAngle = 128.7976f,
SpotlightIntensity = 25f,
VlbIntensity = 0.03f,
VlbNoiseEnabled = false,
ParticlesAlpha = 0.1f,
ParticlesDensity = 15,
ParticlesDistanceRange = new MinMaxRangeFloat(0.05f, 0.7f)
};
public static readonly TorchProperties AimingDefaults = new()
{
ColliderAngles = 20f,
InnerSpotAngle = 21.80208f,
OuterSpotAngle = 30f,
SpotlightIntensity = 25f,
VlbIntensity = 0.5f,
VlbNoiseEnabled = true,
ParticlesAlpha = 0.2f,
ParticlesDensity = 2,
ParticlesDistanceRange = new MinMaxRangeFloat(0f, 0.3f)
};
public static readonly TorchProperties FocusDefaults = new()
{
ColliderAngles = 7f,
InnerSpotAngle = 0f,
OuterSpotAngle = 8.197921f,
SpotlightIntensity = 175f,
VlbIntensity = 0.7f,
VlbNoiseEnabled = true,
ParticlesAlpha = 0.2f,
ParticlesDensity = 2,
ParticlesDistanceRange = new MinMaxRangeFloat(0f, 0.3f)
};
}
[Serializable]
public struct TorchSpeedSettings
{
[Min(0f)]
public float ColliderSpeed;
[Min(0f)]
public float SpotlightSpotAngleSpeed;
[Min(0f)]
public float VlbSpotAngleSpeed;
[Min(0f)]
public float SpotlightIntensitySpeed;
[Min(0f)]
public float VlbIntensitySpeed;
public static readonly TorchSpeedSettings IdleDefaults = new()
{
ColliderSpeed = 35f,
SpotlightSpotAngleSpeed = 45f,
VlbSpotAngleSpeed = 35f,
SpotlightIntensitySpeed = 25f,
VlbIntensitySpeed = 1f
};
public static readonly TorchSpeedSettings IdleAimingDefaults = new()
{
ColliderSpeed = 70f,
SpotlightSpotAngleSpeed = 90f,
VlbSpotAngleSpeed = 70f
};
public static readonly TorchSpeedSettings AimingDefaults = new()
{
ColliderSpeed = 35f,
SpotlightSpotAngleSpeed = 45f,
VlbSpotAngleSpeed = 35f
};
public static readonly TorchSpeedSettings FocusDefaults = new()
{
ColliderSpeed = 20f,
SpotlightSpotAngleSpeed = 20f,
VlbSpotAngleSpeed = 25f
};
public static readonly TorchSpeedSettings FocusSuccessDefaults = new()
{
ColliderSpeed = 45f,
SpotlightSpotAngleSpeed = 45f,
VlbSpotAngleSpeed = 45f
};
}
[Serializable]
public struct AnimationCurveEase
{
public Ease Ease;
public bool UseAnimationCurve;
public AnimationCurve Curve;
public static readonly AnimationCurveEase InOutSine = new()
{
Ease = Ease.InOutSine,
Curve = AnimationCurve.EaseInOut(0,0,1,1)
};
public static readonly AnimationCurveEase InOutElastic = new()
{
Ease = Ease.InOutElastic,
Curve = AnimationCurve.EaseInOut(0,0,1,1)
};
}
[Serializable]
public struct AimControllerProperties
{
[Min(0f)]
[Tooltip("Speed of turning towards the target using Vector3.RotateTowards.")]
public float MaxRadiansDelta;
[Min(0f)]
[Tooltip("Speed of moving towards the target using Vector3.RotateTowards.")]
public float MaxMagnitudeDelta;
[Min(0f)]
[Tooltip("Speed of slerping towards the target.")]
public float SlerpSpeed;
public static readonly AimControllerProperties IdleAiming = new()
{
MaxRadiansDelta = 0.3f,
MaxMagnitudeDelta = 0.3f,
SlerpSpeed = 0.5f
};
public static readonly AimControllerProperties Aiming = new()
{
MaxRadiansDelta = 3f,
MaxMagnitudeDelta = 3f,
SlerpSpeed = 3f
};
}
#endregion
#region Variables and Properties
[SerializeField] private Animator anim;
[SerializeField] private AimController aim;
[SerializeField] private TriggerSensor torchSensor;
[SerializeField] private FOVCollider torchCollider;
[SerializeField] private Light torchSpotlight;
[SerializeField] private VolumetricLightBeam torchVlb;
[SerializeField] private EffectFlicker torchFlicker;
[SerializeField] private VolumetricDustParticles torchParticles;
[Space]
[SerializeField] private string hitboxTag;
[SerializeField] private Transform idleTarget;
public float FocusStartDelay = 0.3f;
public float FocusCooldownDuration = 1f;
public float AimAfterIdleAimDelay = 0.5f;
[Header("State Settings")]
public TorchProperties IdleProperties = TorchProperties.IdleDefaults;
public TorchSpeedSettings IdleSpeedSettings = TorchSpeedSettings.IdleDefaults;
public AnimationCurveEase IdleEase = AnimationCurveEase.InOutSine;
[Space]
public TorchProperties IdleAimingProperties = TorchProperties.IdleAimingDefaults;
public TorchSpeedSettings IdleAimingSpeedSettings = TorchSpeedSettings.IdleAimingDefaults;
public AnimationCurveEase IdleAimingEase = AnimationCurveEase.InOutSine;
[Space]
public TorchProperties AimingProperties= TorchProperties.AimingDefaults;
public TorchSpeedSettings AimingSpeedSettings = TorchSpeedSettings.AimingDefaults;
public AnimationCurveEase AimingEase = AnimationCurveEase.InOutSine;
[Space]
public TorchProperties FocusProperties = TorchProperties.FocusDefaults;
public TorchSpeedSettings FocusSpeedSettings = TorchSpeedSettings.FocusDefaults;
public AnimationCurveEase FocusEase = AnimationCurveEase.InOutSine;
[Space]
public TorchSpeedSettings FocusSuccessSpeedSettings = TorchSpeedSettings.FocusSuccessDefaults;
public AnimationCurveEase FocusSuccessEase = AnimationCurveEase.InOutElastic;
[Header("Aim Settings")]
public AimControllerProperties NonProminentAimProperties = AimControllerProperties.IdleAiming;
public AimControllerProperties ProminentAimProperties = AimControllerProperties.Aiming;
[Header("Debug Settings")]
[SerializeField] private bool verbose;
private AimTargetController targetController;
private Coroutine torchRoutine;
private StoppableCoroutine torchNestedRoutine;
private Tween colliderAnglesTween, outerSpotAngleTween, innerSpotAngleTween, vlbSpotAngleTween, spotlightIntensityTween, vlbIntensityTween;
private EventDispatcher eventDispatcher;
private GameObject toBeFocusAim, toBeFocusCooldown;
private AimActorState aimState;
private bool initialized, isFocusing, isFocusCoolingDown;
public Animator Anim => anim;
public AimController Aim => aim;
public TriggerSensor TorchSensor => torchSensor;
public Light TorchSpotlight => torchSpotlight;
public VolumetricLightBeam TorchVlb => torchVlb;
public string HitboxTag => hitboxTag;
public Transform IdleTarget => idleTarget;
public bool TorchAimed { get; private set; }
public AimActorState AimState
{
get => aimState;
private set
{
if (aimState == value) return;
#if UNITY_EDITOR
if (verbose) Debug.Log(name + " changing state from " + aimState + " to " + value);
#endif
aimState = value;
}
}
#endregion
public void Init(AimTargetController controller)
{
if (initialized || !controller) return;
targetController = controller;
// force some settings
torchVlb.trackChangesDuringPlaytime = true;
torchVlb.intensityFromLight = false;
torchVlb.spotAngleFromLight = false;
torchFlicker.returnToStartValuesAfterFlicker = true;
torchFlicker.restoreBaseIntensity = false;
initialized = true;
}
#region MonoBehaviour Methods
private void OnEnable()
{
eventDispatcher = FungusManager.Instance.EventDispatcher;
}
private void OnDisable()
{
eventDispatcher = null;
}
private void OnValidate()
{
if (anim == null)
anim = GetComponent<Animator>();
if (aim == null)
aim = GetComponent<AimController>();
if (torchSensor == null)
torchSensor = GetComponentInChildren<TriggerSensor>();
if (torchCollider == null)
torchCollider = GetComponentInChildren<FOVCollider>();
if (torchSpotlight == null)
torchSpotlight = GetComponentInChildren<Light>();
if (torchVlb == null)
torchVlb = GetComponentInChildren<VolumetricLightBeam>();
if (torchFlicker == null)
torchFlicker = GetComponentInChildren<EffectFlicker>();
if (torchParticles == null)
torchParticles = GetComponentInChildren<VolumetricDustParticles>();
}
private void Update()
{
if (!initialized) return;
SetAimControllerValues();
// check if we can now focus on objects that were previously rejected
if (toBeFocusAim && TorchAimed)
FocusTorch(toBeFocusAim, null);
else if (toBeFocusCooldown && !isFocusCoolingDown)
FocusTorch(toBeFocusCooldown, null);
}
#endregion
#region Public Methods
/// <summary>
/// Used when the torch is not being aimed by the actor in control.
/// </summary>
/// <param name="instant">Instantly apply values instead of tweening.</param>
public void IdleTorch(bool instant = false)
{
if (!initialized || AimState == AimActorState.Idle) return;
MaybeKillTorchRoutine();
TorchAimed = false;
AimState = AimActorState.Idle;
if (instant)
SetTorchValuesInstantly(IdleProperties);
else
torchRoutine = StartCoroutine(TorchRoutine(IdleProperties, IdleSpeedSettings, IdleEase, false, true));
}
/// <summary>
/// Used for providing ambient fill light over the scene while the actor is not in control.
/// </summary>
/// <param name="instant">Instantly apply values instead of tweening.</param>
public void IdleAimTorch(bool instant = false)
{
if (!initialized || AimState == AimActorState.IdleAiming) return;
MaybeKillTorchRoutine();
torchFlicker.enabled = false;
TorchAimed = false;
AimState = AimActorState.IdleAiming;
if (instant)
SetTorchValuesInstantly(IdleAimingProperties);
else
torchRoutine = StartCoroutine(IdleAimRoutine());
}
/// <summary>
/// Used for aiming after idle aiming.
/// </summary>
public void AimTorchAfterIdleAim()
{
if (!initialized) return;
MaybeKillTorchRoutine();
TorchAimed = false;
AimState = AimActorState.AimingAfterIdleAim;
torchRoutine = StartCoroutine(AimTorchAfterIdleRoutine());
}
/// <summary>
/// Used for aiming after idle.
/// </summary>
/// <param name="instant">Instantly apply values instead of tweening.</param>
public void AimTorchAfterIdle(bool instant = false)
{
if (!initialized || AimState is AimActorState.Aiming or AimActorState.AimingAfterIdleAim) return;
MaybeKillTorchRoutine();
TorchAimed = false;
AimState = AimActorState.Aiming;
if (instant)
SetTorchValuesInstantly(AimingProperties);
else
torchRoutine = StartCoroutine(AimTorchRoutine());
}
/// <summary>
/// Used for aiming after focusing.
/// </summary>
/// <param name="go">GameObject that lost focus.</param>
/// <param name="sensor">Not used.</param>
public void AimTorchAfterFocusing(GameObject go, Sensor sensor)
{
if (!initialized || AimState == AimActorState.Aiming) return;
// reset toBeFocus variables
if (go == toBeFocusAim)
toBeFocusAim = null;
if (go == toBeFocusCooldown)
toBeFocusCooldown = null;
if (AimState == AimActorState.AimingAfterIdleAim) return;
// left focusing state before it was successful. apply a cooldown on selecting objects. but only if not in lock on mode so we dont delay selection.
if (AimState == AimActorState.Focusing && isFocusing && !targetController.LockOnToggled)
StartCoroutine(FocusCooldownRoutine());
MaybeKillTorchRoutine();
TorchAimed = false;
AimState = AimActorState.Aiming;
torchRoutine = StartCoroutine(AimTorchRoutine());
}
/// <summary>
/// Used to focus on an interactable GameObject.
/// Called by the TriggerSensor upon detecting an selectable.
/// </summary>
/// <param name="go">GameObject to begin focusing on.</param>
/// <param name="sensor">Not used.</param>
public void FocusTorch(GameObject go, Sensor sensor)
{
if (!initialized || AimState == AimActorState.Focusing || !GameStateManager.HasControl) return;
// require the torch to be finished aiming before we can start focusing.
// if we aren't aimed yet store what we passed in and try again with it later.
if (!TorchAimed)
{
toBeFocusAim = go;
return;
}
// check if the torch is cooling down after an unsuccessful focus attempt.
// if we aren't cooled down yet store what we passed in and try again with it later.
if (isFocusCoolingDown)
{
toBeFocusCooldown = go;
return;
}
// get and check if the selectable is interactable
var selectable = go.GetComponent<SceneSelectable>();
if (!selectable || !selectable.Interactable) return;
//raise SelectableDetected fungus event
eventDispatcher.Raise(new SelectableDetected.DetectEvent(selectable));
// check that what we've started to focus on has its conditionals fulfilled
if (!selectable.ConditionalsFulfilled) return;
MaybeKillTorchRoutine();
torchFlicker.enabled = false; // dont allow flickering of the torch while focusing
AimState = AimActorState.Focusing;
torchRoutine = StartCoroutine(FocusTorchRoutine(selectable));
}
#endregion
#region Private Coroutines
/// <summary>
/// Set/Tween the 8 different values that determine the torches look.
/// </summary>
private IEnumerator TorchRoutine(TorchProperties properties, TorchSpeedSettings speedSettings, AnimationCurveEase ease, bool waitForLight = false, bool intensitySpeedBased = false)
{
// use booleans to signal completion instead of yielding to WaitForCompletion
// because if a tween is already finished before its reached it will throw a warning.
bool colliderComplete = false;
bool innerSpotComplete = false;
bool outerSpotComplete = false;
bool vlbSpotComplete = false;
// set vlb's noise mode
torchVlb.noiseMode = properties.VlbNoiseEnabled ? NoiseMode.LocalSpace : NoiseMode.Disabled;
// tween the fov collider
bool runColliderTween = !Mathf.Approximately(torchCollider.FOVAngle, properties.ColliderAngles);
if (runColliderTween)
{
float angleValue = torchCollider.FOVAngle;
colliderAnglesTween = DOTween.To(x => angleValue = x, torchCollider.FOVAngle, properties.ColliderAngles, speedSettings.ColliderSpeed)
.SetSpeedBased()
.OnUpdate(() =>
{
var value = Mathf.Clamp(angleValue, 1, 180);
torchCollider.FOVAngle = value;
torchCollider.ElevationAngle = value;
torchCollider.CreateCollider();
})
.OnComplete(() => colliderComplete = true);
if (ease.UseAnimationCurve)
colliderAnglesTween.SetEase(ease.Curve);
else
colliderAnglesTween.SetEase(ease.Ease);
}
else
{
torchCollider.FOVAngle = properties.ColliderAngles;
torchCollider.ElevationAngle = properties.ColliderAngles;
torchCollider.CreateCollider();
}
// tween the spotlight's inner spot angle
var innerSpotAngleDuration = 0f;
innerSpotAngleTween = DOTween.To(x => torchSpotlight.innerSpotAngle = x, torchSpotlight.innerSpotAngle, properties.InnerSpotAngle, speedSettings.SpotlightSpotAngleSpeed)
.SetSpeedBased()
.OnStart(() => innerSpotAngleDuration = innerSpotAngleTween.Duration())
.OnComplete(() => innerSpotComplete = true);
if (ease.UseAnimationCurve)
innerSpotAngleTween.SetEase(ease.Curve);
else
innerSpotAngleTween.SetEase(ease.Ease);
// tween the spotlight's outer spot angle
outerSpotAngleTween = DOTween.To(x => torchSpotlight.spotAngle = x, torchSpotlight.spotAngle, properties.OuterSpotAngle, speedSettings.SpotlightSpotAngleSpeed)
.SetSpeedBased()
.OnComplete(() => outerSpotComplete = true);
if (ease.UseAnimationCurve)
outerSpotAngleTween.SetEase(ease.Curve);
else
outerSpotAngleTween.SetEase(ease.Ease);
// tween the vlb's spot angle
vlbSpotAngleTween = DOTween.To(x => torchVlb.spotAngle = x, torchVlb.spotAngle, properties.OuterSpotAngle, speedSettings.VlbSpotAngleSpeed)
.SetSpeedBased()
.OnComplete(() => vlbSpotComplete = true);
if (ease.UseAnimationCurve)
vlbSpotAngleTween.SetEase(ease.Curve);
else
vlbSpotAngleTween.SetEase(ease.Ease);
innerSpotAngleTween.Play();
outerSpotAngleTween.Play();
vlbSpotAngleTween.Play();
yield return null;
if (!intensitySpeedBased)
{
// tween the spotlight's intensity
spotlightIntensityTween = torchSpotlight.DOIntensity(properties.SpotlightIntensity, innerSpotAngleDuration);
if (ease.UseAnimationCurve)
spotlightIntensityTween.SetEase(ease.Curve);
else
spotlightIntensityTween.SetEase(ease.Ease);
// tween the vlb's intensity
vlbIntensityTween = DOTween.To(x => torchVlb.intensityGlobal = x, torchVlb.intensityGlobal, properties.VlbIntensity, innerSpotAngleDuration);
if (ease.UseAnimationCurve)
vlbIntensityTween.SetEase(ease.Curve);
else
vlbIntensityTween.SetEase(ease.Ease);
}
else
{
// tween the spotlight's intensity
spotlightIntensityTween = torchSpotlight.DOIntensity(properties.SpotlightIntensity, speedSettings.SpotlightIntensitySpeed)
.SetSpeedBased();
if (ease.UseAnimationCurve)
spotlightIntensityTween.SetEase(ease.Curve);
else
spotlightIntensityTween.SetEase(ease.Ease);
// tween the vlb's intensity
vlbIntensityTween = DOTween.To(x => torchVlb.intensityGlobal = x, torchVlb.intensityGlobal, properties.VlbIntensity, speedSettings.VlbIntensitySpeed)
.SetSpeedBased();
if (ease.UseAnimationCurve)
vlbIntensityTween.SetEase(ease.Curve);
else
vlbIntensityTween.SetEase(ease.Ease);
}
// set particles values
torchParticles.alpha = properties.ParticlesAlpha;
torchParticles.density = properties.ParticlesDensity;
torchParticles.spawnDistanceRange = properties.ParticlesDistanceRange;
if (runColliderTween)
yield return new WaitUntil(() => colliderComplete);
if (waitForLight)
yield return new WaitUntil(() => innerSpotComplete && outerSpotComplete && vlbSpotComplete);
}
private IEnumerator IdleAimRoutine()
{
torchParticles.enabled = false; // disable to restart simulation once values are adjusted
torchNestedRoutine = StartStoppableCoroutine(TorchRoutine(IdleAimingProperties, IdleAimingSpeedSettings, IdleAimingEase, true));
yield return torchNestedRoutine.WaitFor();
torchNestedRoutine = null;
torchParticles.enabled = true;
}
private IEnumerator AimTorchRoutine()
{
torchNestedRoutine = StartStoppableCoroutine(TorchRoutine(AimingProperties, AimingSpeedSettings, AimingEase));
yield return torchNestedRoutine.WaitFor();
torchNestedRoutine = null;
TorchAimed = true;
}
/// <summary>
/// Briefly kill the torch's light to swap values from idle mode to aim mode.
/// </summary>
private IEnumerator AimTorchAfterIdleRoutine()
{
torchSpotlight.enabled = false;
torchVlb.enabled = false;
torchParticles.enabled = false; // disable the component since we WANT the simulation to restart.
SetTorchValuesInstantly(AimingProperties);
yield return new WaitForSeconds(AimAfterIdleAimDelay);
torchSpotlight.enabled = true;
torchVlb.enabled = true;
torchParticles.enabled = true;
TorchAimed = true;
AimState = AimActorState.Aiming;
}
private IEnumerator FocusTorchRoutine(SceneSelectable selectable)
{
yield return new WaitForSeconds(FocusStartDelay);
isFocusing = true;
torchParticles.SetParticlesVisibility(false);
torchNestedRoutine = StartStoppableCoroutine(TorchRoutine(FocusProperties, FocusSpeedSettings, FocusEase));
yield return torchNestedRoutine.WaitFor();
torchNestedRoutine = null;
isFocusing = false;
FungusPrioritySignals.DoIncreasePriorityDepth(); // revoke control from player
// lock into position to ensure close to same lighting on objects for all players, if we opted in.
if (selectable.LockOnWhenFocused)
aim.target = selectable.GetFocusPoint(); // set AimController's target itself instead of the aim target position so we get smoothing between the two targets.
innerSpotAngleTween?.Kill();
outerSpotAngleTween?.Kill();
vlbSpotAngleTween?.Kill();
spotlightIntensityTween?.Kill();
vlbIntensityTween?.Kill();
torchNestedRoutine = StartStoppableCoroutine(TorchRoutine(AimingProperties, FocusSuccessSpeedSettings, FocusSuccessEase));
torchParticles.SetParticlesVisibility(true);
yield return torchNestedRoutine.WaitFor();
torchNestedRoutine = null;
//raise SelectableFocused fungus event
eventDispatcher.Raise(new SelectableFocused.FocusEvent(selectable));
TorchAimed = true;
AimState = AimActorState.Aiming;
}
private IEnumerator FocusCooldownRoutine()
{
isFocusCoolingDown = true;
yield return new WaitForSeconds(FocusCooldownDuration);
isFocusCoolingDown = false;
}
#endregion
#region Private Methods
private void SetTorchValuesInstantly(TorchProperties properties)
{
torchCollider.FOVAngle = properties.ColliderAngles;
torchCollider.ElevationAngle = properties.ColliderAngles;
torchCollider.CreateCollider();
torchSpotlight.spotAngle = properties.OuterSpotAngle;
torchSpotlight.innerSpotAngle = properties.InnerSpotAngle;
torchSpotlight.intensity = properties.SpotlightIntensity;
torchVlb.spotAngle = properties.OuterSpotAngle;
torchVlb.intensityGlobal = properties.VlbIntensity;
torchVlb.noiseMode = properties.VlbNoiseEnabled ? NoiseMode.LocalSpace : NoiseMode.Disabled;
torchParticles.alpha = properties.ParticlesAlpha;
torchParticles.density = properties.ParticlesDensity;
torchParticles.spawnDistanceRange = properties.ParticlesDistanceRange;
}
/// <summary>
/// Set speed values on the Actor's Aim Target Controller.
/// </summary>
private void SetAimControllerValues()
{
if (targetController.FollowAimTargetOnIdle)
{
if (targetController.ProminentActor == this)
{
aim.maxRadiansDelta = ProminentAimProperties.MaxRadiansDelta;
aim.maxMagnitudeDelta = ProminentAimProperties.MaxMagnitudeDelta;
aim.slerpSpeed = ProminentAimProperties.SlerpSpeed;
}
else
{
aim.maxRadiansDelta = NonProminentAimProperties.MaxRadiansDelta;
aim.maxMagnitudeDelta = NonProminentAimProperties.MaxMagnitudeDelta;
aim.slerpSpeed = NonProminentAimProperties.SlerpSpeed;
}
}
else
{
aim.maxRadiansDelta = ProminentAimProperties.MaxRadiansDelta;
aim.maxMagnitudeDelta = ProminentAimProperties.MaxMagnitudeDelta;
aim.slerpSpeed = ProminentAimProperties.SlerpSpeed;
}
}
private void MaybeKillTorchRoutine()
{
if (torchRoutine != null)
StopCoroutine(torchRoutine);
torchNestedRoutine?.Stop();
torchNestedRoutine = null;
colliderAnglesTween?.Kill();
innerSpotAngleTween?.Kill();
outerSpotAngleTween?.Kill();
vlbSpotAngleTween?.Kill();
spotlightIntensityTween?.Kill();
vlbIntensityTween?.Kill();
torchParticles.SetParticlesVisibility(true);
isFocusing = false;
torchSpotlight.enabled = true;
torchVlb.enabled = true;
torchParticles.enabled = true;
torchFlicker.enabled = true;
toBeFocusAim = null;
toBeFocusCooldown = null;
}
#endregion
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Fungus;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace HumbleGrove.SpaceCapitalism
{
public class AimTargetController : MonoBehaviour
{
[SerializeField] private AimActor[] actors;
[SerializeField] private Transform aimTarget;
[SerializeField] private new Camera camera;
[Header("General Settings")]
public bool ControlBothActors = true;
[SerializeField] private bool holdButtonToAim = true;
[SerializeField] private bool followAimTargetOnIdle;
[Header("Mouse Settings")]
[Min(0f)]
[SerializeField] private float raycastLength = 10000;
[SerializeField] private LayerMask hitboxLayer = 7;
[Min(0f)]
[SerializeField] private float breakLockOnDistance = 200f;
[Header("Controller Settings")]
[Min(0f)]
[SerializeField] private float defaultSplineHeight = 10f;
[Min(0f)]
[SerializeField] private float switchActorsBuffer = 0.1f;
[Min(0f)]
[SerializeField] private float splineXSpeed = 1;
[Min(0f)]
[SerializeField] private float splinePrecisionXSpeed = 0.5f;
[Min(0f)]
[SerializeField] private float splineYSpeed = 15;
[Min(0f)]
[SerializeField] private float splinePrecisionYSpeed = 7.5f;
[Min(0f)]
[SerializeField] private float breakLockOnLength = 0.8f;
[Header("Optional")]
[SerializeField] private TrackedDollyController trackedDollyController;
// tutorial stuff
[HideInInspector] public bool CanAim = true;
[HideInInspector] public bool ForceLockOnModeOnly;
private bool initalized, torchToggled, precisionModeToggled, lockOnToggled, usingControllerLastFrame;
private AimActor prominentActor;
private AimTargetSplineController aimTargetSplineController;
private List<SceneSelectable> selectables = new();
private int lockedOnIndex;
private Vector2 mousePositionBeforeLockOn;
#if UNITY_EDITOR
private bool debugPreventInput;
#endif
private static readonly int HasTargetID = Animator.StringToHash("HasTarget");
public AimActor ProminentActor => prominentActor;
public bool LockOnToggled => lockOnToggled;
public bool PrecisionModeToggled => precisionModeToggled;
public bool FollowAimTargetOnIdle => followAimTargetOnIdle;
#region MonoBehaviour Methods
private void Start()
{
if (initalized) return;
// check that we have the values we need and abort if we dont
if (actors.Length != 2)
{
Debug.LogError("AimTargetController requires 2 actors, aborting initialization. If you wish to control only one actor set ControlBothActors to False");
return;
}
aimTargetSplineController = aimTarget.GetComponent<AimTargetSplineController>();
if (aimTargetSplineController == null)
{
Debug.LogError("AimTargetController's AimTarget doesn't have a AimTargetSplineController, aborting initialization.");
return;
}
aimTargetSplineController.enabled = false;
if (!camera)
{
Debug.LogError("AimTargetController's doesn't have a Camera assigned, aborting initialization.");
return;
}
// initialize the actors and set the proper state
for (var i = 0; i < actors.Length; i++)
{
var actor = actors[i];
actor.Init(this);
if (i == 0)
{
prominentActor = actor;
prominentActor.Aim.target = aimTarget;
prominentActor.Aim.weight = 0;
prominentActor.Anim.SetBool(HasTargetID, false);
prominentActor.IdleTorch(true);
}
else
{
actor.Aim.target = followAimTargetOnIdle ? aimTarget : actor.IdleTarget;
actor.Aim.weight = 1;
actor.Anim.SetBool(HasTargetID, true);
actor.IdleAimTorch(true);
}
}
AddSensorListeners();
// get all the selectables in the scene and subscribe to changes to them
EventManager.AddListener(EventManager.REFRESH_SELECTABLES, UpdateSelectablesPool);
UpdateSelectablesPool();
// configure stuff that is changed via player options and subscribe to any changes to them
EventManager.AddListener(EventManager.OPTION_CHANGED, UpdateOptions);
UpdateOptions();
FungusPrioritySignals.OnFungusPriorityEnd += OnPriorityEnd;
initalized = true;
}
private void OnDestroy()
{
EventManager.RemoveListener(EventManager.REFRESH_SELECTABLES, UpdateSelectablesPool);
EventManager.RemoveListener(EventManager.OPTION_CHANGED, UpdateOptions);
FungusPrioritySignals.OnFungusPriorityEnd -= OnPriorityEnd;
}
private void Update()
{
#if UNITY_EDITOR
// toggle locking the torch into place for debugging
if (Input.GetKeyDown(KeyCode.L))
debugPreventInput = !debugPreventInput;
if (debugPreventInput) return;
#endif
if (!initalized || !GameStateManager.HasControl || !CanAim || GameStateManager.IsPaused) return;
bool isDefaultMap = InputManager.Instance.GetCurrentRuleset() == "Default"; // hack cause aim torch is being triggered outside of the default map (shrug)
// toggle torch if the torch activation mode is toggle
if (!holdButtonToAim)
{
if (InputManager.Player.GetButtonDown(RewiredConsts.Action.AimTorch) && isDefaultMap)
torchToggled = !torchToggled;
}
var torchInput = GetTorchInput() && isDefaultMap;
// only allow camera movement when torch isn't aimed
if (trackedDollyController)
trackedDollyController.enabled = !torchInput;
if (DataManager.GetOptionBoolean(Values.USING_CONTROLLER))
{
// enter controller mode if needed
if (!usingControllerLastFrame)
{
lockOnToggled = false;
precisionModeToggled = false;
torchToggled = false;
// reset the position of the aim target on the spline
aimTargetSplineController.enabled = true;
aimTargetSplineController.RelativePosition = ControlBothActors ? prominentActor == actors[0] ? 0.25f : 0.75f : 0.5f;
aimTargetSplineController.Height = defaultSplineHeight;
prominentActor.Aim.target = aimTarget;
IdleProminentActorTorch();
usingControllerLastFrame = true;
}
// extract stick values
var x = InputManager.Player.GetAxis(RewiredConsts.Action.TorchHorizontal);
var y = InputManager.Player.GetAxis(RewiredConsts.Action.TorchVertical);
if (DataManager.GetOptionBoolean(Values.INVERT_AIM_Y)) // maybe invert y axis
y *= -1;
// let locking on take priority over manually aiming
#region Lock On Mode
if (lockOnToggled)
{
if (new Vector2(x, y).magnitude >= breakLockOnLength || !AreAnySelectableInteractable()) // maybe break lock state
{
aimTargetSplineController.enabled = true;
lockOnToggled = false;
}
else
{
UpdateLockOnTarget();
return;
}
}
else if (!lockOnToggled && (InputManager.Player.GetButtonDown(RewiredConsts.Action.TorchSelectNext) || InputManager.Player.GetButtonDown(RewiredConsts.Action.TorchSelectPrevious))) // maybe enter lock on mode
{
if (AreAnySelectableInteractable())
{
aimTargetSplineController.enabled = false;
lockOnToggled = true;
lockedOnIndex = GetIndexOfFirstInteractableSelectable();
if (trackedDollyController)
trackedDollyController.enabled = false;
if (ControlBothActors)
{
var maybeNewActor = selectables[lockedOnIndex].LockOnActor;
if (prominentActor != maybeNewActor)
ChangeProminentActor(maybeNewActor);
}
AimProminentActorTorch();
aimTarget.transform.position = selectables[lockedOnIndex].GetFocusPoint().position;
return;
}
else
{
//here is place for an sfx warning the player that nothing was interactable
return;
}
}
#endregion
if (ControlBothActors)
{
// change actors if the aiming spline's relative position has passed its threshold to do so.
if (aimTargetSplineController.RelativePosition < 0.5f - switchActorsBuffer && prominentActor != actors[0])
ChangeProminentActor(actors[0]);
else if (aimTargetSplineController.RelativePosition >= 0.5f + switchActorsBuffer && prominentActor != actors[1])
ChangeProminentActor(actors[1]);
}
if (torchInput && !ForceLockOnModeOnly) // check that the proper input is being made for the current torch activation mode
{
AimProminentActorTorch(); // aim the torch, there is no layer check here like the raycasting below, we are just always aiming when we use controller.
if (prominentActor.TorchAimed) // during the moment when prominent actor changes and the torch isn't aimed don't allow spline movement
{
if (InputManager.Player.GetButtonDown(RewiredConsts.Action.PrecisionAim)) // maybe toggle precision aim
precisionModeToggled = !precisionModeToggled;
// adjust aim target relative position along spline
var xSpeed = precisionModeToggled ? splinePrecisionXSpeed : splineXSpeed;
if (x != 0)
aimTargetSplineController.RelativePosition = Mathf.Clamp01(aimTargetSplineController.RelativePosition + x * xSpeed * Time.deltaTime);
// adjust aim target height along spline
var ySpeed = precisionModeToggled ? splinePrecisionYSpeed : splineYSpeed;
if (y != 0)
aimTargetSplineController.Height = Mathf.Clamp(aimTargetSplineController.Height + y * ySpeed * Time.deltaTime, 0f, aimTargetSplineController.MaxHeight);
}
}
else // torch input isn't being made
{
IdleProminentActorTorch();
}
}
else
{
// exit controller mode if needed
if (usingControllerLastFrame)
{
aimTargetSplineController.enabled = false;
usingControllerLastFrame = false;
}
// let locking on take priority over manually aiming
#region Lock On Mode
if (lockOnToggled)
{
if (Vector2.Distance(mousePositionBeforeLockOn, Input.mousePosition) > breakLockOnDistance || InputManager.Player.GetButtonDown(RewiredConsts.Action.AimTorch) || !AreAnySelectableInteractable()) // maybe break lock on state
{
lockOnToggled = false;
}
else
{
UpdateLockOnTarget();
return;
}
}
else if (!lockOnToggled && (InputManager.Player.GetButtonDown(RewiredConsts.Action.TorchSelectNext) || InputManager.Player.GetButtonDown(RewiredConsts.Action.TorchSelectPrevious))) // maybe enter lock on mode
{
if (AreAnySelectableInteractable())
{
lockOnToggled = true;
lockedOnIndex = GetIndexOfFirstInteractableSelectable();
mousePositionBeforeLockOn = Input.mousePosition;
if (trackedDollyController)
trackedDollyController.enabled = false;
if (ControlBothActors)
{
var maybeNewActor = selectables[lockedOnIndex].LockOnActor;
if (prominentActor != maybeNewActor)
ChangeProminentActor(maybeNewActor);
}
AimProminentActorTorch();
aimTarget.transform.position = selectables[lockedOnIndex].GetFocusPoint().position;
return;
}
else
{
//here is place for an sfx warning the player that nothing was interactable
return;
}
}
#endregion
if (torchInput && !ForceLockOnModeOnly) // check that the proper input is being made for the current torch activation mode
{
Ray mouseRay = camera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(mouseRay, out var hit, raycastLength, hitboxLayer)) // check if we're hitting the torch hitbox layer
{
if (ControlBothActors)
{
var detectedActor = String.IsNullOrWhiteSpace(hit.transform.tag) ? actors[0] : actors.FirstOrDefault(actor => hit.transform.CompareTag(actor.HitboxTag));
if (detectedActor && prominentActor != detectedActor)
{
ChangeProminentActor(detectedActor);
}
else
{
aimTarget.transform.position = hit.point;
AimProminentActorTorch();
}
}
else // there is only one actor to control
{
aimTarget.transform.position = hit.point;
AimProminentActorTorch();
}
}
else // did not hit torch hitbox layer
{
IdleProminentActorTorch();
}
}
else // torch input isn't being made
{
IdleProminentActorTorch();
}
}
}
#endregion
#region Private Methods
/// <summary>
/// Is the proper button held/toggled for aiming the torch.
/// </summary>
public bool GetTorchInput()
{
return (holdButtonToAim && InputManager.Player.GetButton(RewiredConsts.Action.AimTorch)) || (!holdButtonToAim && torchToggled);
}
/// <summary>
/// Assign listeners for the the prominent actor's torch sensor for when an object has bee focused/unfocused.
/// </summary>
private void AddSensorListeners()
{
prominentActor.TorchSensor.OnDetected.AddListener(prominentActor.FocusTorch);
prominentActor.TorchSensor.OnLostDetection.AddListener(prominentActor.AimTorchAfterFocusing);
}
/// <summary>
/// Remove listeners for the the prominent actor's torch sensor.
/// </summary>
private void RemoveSensorListeners()
{
prominentActor.TorchSensor.OnDetected.RemoveListener(prominentActor.FocusTorch);
prominentActor.TorchSensor.OnLostDetection.RemoveListener(prominentActor.AimTorchAfterFocusing);
}
private void IdleProminentActorTorch()
{
if (prominentActor.AimState is AimActor.AimActorState.Idle) return;
prominentActor.Aim.weight = 0;
prominentActor.Anim.SetBool(HasTargetID, false);
prominentActor.IdleTorch();
}
private void AimProminentActorTorch()
{
if (prominentActor.AimState is AimActor.AimActorState.Aiming or AimActor.AimActorState.Focusing) return;
prominentActor.Aim.target = aimTarget; // force the aim target back after locking on to an interactable when focused
prominentActor.Aim.weight = 1;
prominentActor.Anim.SetBool(HasTargetID, true);
prominentActor.AimTorchAfterIdle();
}
private void UpdateLockOnTarget()
{
// one last sanity check even tho we do this before the method is possibly called because of the loop below.
if (!AreAnySelectableInteractable()) return;
bool valueChanged = false;
if (InputManager.Player.GetButtonDown(RewiredConsts.Action.TorchSelectPrevious))
{
do // decrease locked on index until we reach a selectable that is interactable
{
lockedOnIndex--;
if (lockedOnIndex <= -1) // wrap value
lockedOnIndex = selectables.Count - 1;
}
while (!selectables[lockedOnIndex].Interactable);
valueChanged = true;
}
else if (InputManager.Player.GetButtonDown(RewiredConsts.Action.TorchSelectNext))
{
do // increase locked on index until we reach a selectable that is interactable
{
lockedOnIndex++;
if (lockedOnIndex > selectables.Count - 1) // wrap value
lockedOnIndex = 0;
}
while (!selectables[lockedOnIndex].Interactable);
valueChanged = true;
}
if (valueChanged)
{
if (ControlBothActors)
{
// check if the new interactable has a different actor than the one that is currently prominent
var maybeNewActor = selectables[lockedOnIndex].LockOnActor;
if (prominentActor != maybeNewActor)
ChangeProminentActor(maybeNewActor);
}
// set aim target position to the selectable's focus point
aimTarget.transform.position = selectables[lockedOnIndex].GetFocusPoint().position;
}
}
/// <summary>
/// Clear and get all currently active selectable GameObjects (that have been properly setup).
/// Called when the 'refresh-selectables' event is called.
/// </summary>
private void UpdateSelectablesPool()
{
selectables.Clear();
foreach (var maybeSelectable in GameObject.FindGameObjectsWithTag("Selectable"))
{
var selectable = maybeSelectable.GetComponent<SceneSelectable>();
if (selectable == null) continue;
if (selectable.LockOnActor != null)
{
// was properly configured, add to pool as is
selectables.Add(selectable);
}
else
{
// wasn't configured. populate lock on actor with the first actor
#if UNITY_EDITOR
Debug.LogWarning($"{selectable.name} didn't have its LockOnActor specified, assigning to {actors[0].name}");
#endif
selectable.LockOnActor = actors[0];
selectables.Add(selectable);
}
}
}
/// <summary>
/// Checks if any selectables in the current pool are interactable.
/// </summary>
private bool AreAnySelectableInteractable()
{
return selectables.Any(selectable => selectable.Interactable);
}
/// <summary>
/// Returns the index of the first selectable in the current pool that is interactable
/// </summary>
private int GetIndexOfFirstInteractableSelectable()
{
for (var i = 0; i < selectables.Count; i++)
{
var selectable = selectables[i];
if (selectable.Interactable)
return i;
}
return 0;
}
private void OnPriorityEnd()
{
prominentActor.Aim.target = aimTarget; // set the aim target back after locking on to an interactable when focused
}
private void UpdateOptions()
{
holdButtonToAim = !DataManager.GetOptionBoolean(Values.TOGGLE_AIM);
torchToggled = false;
}
#endregion
#region Public Methods
/// <summary>
/// Change the actor that is aiming at the AimTarget.
/// </summary>
/// <param name="newActor">The actor to change to, must be initialized with this AimTargetController.</param>
public void ChangeProminentActor(AimActor newActor)
{
if (!initalized) return;
if (!actors.Contains(newActor))
{
Debug.LogWarning("Attempted to set the prominent actor to an actor not initialized with this AimTargetController, aborting.");
return;
}
RemoveSensorListeners();
prominentActor.Aim.target = followAimTargetOnIdle ? aimTarget : prominentActor.IdleTarget; // either follow along with the aim target while idle or idle at a predefined position
prominentActor.Aim.weight = 1;
prominentActor.IdleAimTorch();
prominentActor.Anim.SetBool(HasTargetID, true);
newActor.Aim.target = aimTarget;
newActor.AimTorchAfterIdleAim();
prominentActor = newActor;
AddSensorListeners();
}
// used from the tutorial only
public void ResetTorchToggle()
{
torchToggled = false;
}
#endregion
#region Editor Only Methods
#if UNITY_EDITOR
public void CopyValuesFromMainActor()
{
if (actors.Length <= 1)
{
Debug.LogWarning("Unable to copy values from main actor, 2 or more actors are required.");
return;
}
Undo.IncrementCurrentGroup();
Undo.SetCurrentGroupName("Sync Values From Main Actor");
var mainActor = actors[0];
for (var i = 0; i < actors.Length; i++)
{
var actor = actors[i];
if (i != 0)
{
Undo.RecordObject(actor.TorchSpotlight, "Sync Spotlight");
actor.TorchSpotlight.intensity = mainActor.TorchSpotlight.intensity;
actor.TorchSpotlight.spotAngle = mainActor.TorchSpotlight.spotAngle;
actor.TorchSpotlight.innerSpotAngle = mainActor.TorchSpotlight.innerSpotAngle;
actor.TorchSpotlight.range = mainActor.TorchSpotlight.range;
Undo.RecordObject(actor.TorchVlb, "Sync Volumetric Light Beam");
actor.TorchVlb.intensityGlobal = mainActor.TorchVlb.intensityGlobal;
actor.TorchVlb.spotAngle = mainActor.TorchVlb.spotAngle;
actor.TorchVlb.noiseMode = mainActor.TorchVlb.noiseMode;
Undo.RecordObject(actor, "Sync Aim Actor");
actor.FocusStartDelay = mainActor.FocusStartDelay;
actor.AimAfterIdleAimDelay = mainActor.AimAfterIdleAimDelay;
actor.FocusCooldownDuration = mainActor.FocusCooldownDuration;
actor.IdleProperties = mainActor.IdleProperties;
actor.IdleSpeedSettings = mainActor.IdleSpeedSettings;
actor.IdleEase = mainActor.IdleEase;
actor.IdleAimingProperties = mainActor.IdleAimingProperties;
actor.IdleAimingSpeedSettings = mainActor.IdleAimingSpeedSettings;
actor.IdleAimingEase = mainActor.IdleAimingEase;
actor.AimingProperties = mainActor.AimingProperties;
actor.AimingSpeedSettings = mainActor.AimingSpeedSettings;
actor.AimingEase = mainActor.AimingEase;
actor.FocusProperties = mainActor.FocusProperties;
actor.FocusSpeedSettings = mainActor.FocusSpeedSettings;
actor.FocusEase = mainActor.FocusEase;
actor.FocusSuccessSpeedSettings = mainActor.FocusSuccessSpeedSettings;
actor.FocusSuccessEase = mainActor.FocusSuccessEase;
actor.NonProminentAimProperties = mainActor.NonProminentAimProperties;
actor.ProminentAimProperties = mainActor.ProminentAimProperties;
}
}
Debug.Log($"Copied values from main actor ({mainActor.name}) to {actors.Length - 1} actor(s).");
}
#endif
#endregion
}
}
using System.Linq;
using UnityEngine;
namespace HumbleGrove.SpaceCapitalism
{
[RequireComponent(typeof(Collider))]
public class SceneSelectable : MonoBehaviour
{
[Tooltip("Can this selectable be focused on.")]
public bool Interactable = true;
[Tooltip("Should the refresh-selectables event be called upon enabling/disabling.")]
public bool AutomaticallyRefreshSelectables = true;
[Header("Focus Settings")]
[Tooltip("Should the actor lock its aim upon focusing on this selectable. Useful for providing consistent lighting on an object.")]
[SerializeField] private bool lockOnWhenFocused = true;
[Tooltip("Alternative transform to aim at upon focusing on this selectable. If null, the transform of the selectable will be used.")]
[SerializeField] private Transform focusLockOverride;
[Header("Lock On Settings")]
[Tooltip("Actor that will aim at this selectable in lock on mode. If null, the first actor in the AimTargetController will be used.")]
public AimActor LockOnActor;
private ConditionalBase[] conditionals;
public bool LockOnWhenFocused => lockOnWhenFocused;
public bool ConditionalsFulfilled { get; private set; }
private void Awake()
{
// force tag used by the trigger sensor if it's not setup already
if (!CompareTag("Selectable"))
{
#if UNITY_EDITOR
Debug.LogWarning($"{name} didn't have its Tag set to Selectable. assigning now.", gameObject);
#endif
tag = "Selectable";
}
conditionals = GetComponentsInChildren<ConditionalBase>();
}
public void OnEnable()
{
EventManager.AddListener(EventManager.VARIABLE_CHANGED, UpdateConditionalCheck);
EventManager.AddListener(EventManager.GLOBAL_VARIABLE_CHANGED, UpdateConditionalCheck);
UpdateConditionalCheck();
if (AutomaticallyRefreshSelectables)
EventManager.TriggerEvent(EventManager.REFRESH_SELECTABLES);
}
public void OnDisable()
{
EventManager.RemoveListener(EventManager.VARIABLE_CHANGED, UpdateConditionalCheck);
EventManager.RemoveListener(EventManager.GLOBAL_VARIABLE_CHANGED, UpdateConditionalCheck);
if (AutomaticallyRefreshSelectables)
EventManager.TriggerEvent(EventManager.REFRESH_SELECTABLES);
}
/// <summary>
/// Get the transform that the torch should rest on upon completing focusing.
/// </summary>
public Transform GetFocusPoint()
{
return focusLockOverride ? focusLockOverride : transform;
}
/// <summary>
/// Check that all child conditionals evaluate to true and cache the result.
/// </summary>
private void UpdateConditionalCheck()
{
if (conditionals == null || conditionals.Length == 0)
{
ConditionalsFulfilled = true;
return;
}
ConditionalsFulfilled = conditionals.All(conditional => conditional.Evaluate());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment