();
TryGetComponent(out _PlayerPhysics);
TryGetComponent(out _PlayerMaterials);
// We can dash from the start, this should be handled by other behaviour that grants the player
// the ability to dash after completing a task.
canDash = true;
// Starts with disabled trails.
SetActiveTrails(false);
}
public void Dash()
{
if (canDash)
StartCoroutine(CO_Dash());
}
// Used a coroutine to have a WaitForSeconds method to set canDash to true after a given time.
private IEnumerator CO_Dash()
{
canDash = false;
// Enable trails.
SetActiveTrails(true);
_AfterImageHandler.SetActiveAfterImages();
// SetVelocity is being used, if we don't stop it for the duration of the dash,
// AddForce won't have any effect because the velocity will always be set to whatever
// the TadaInput.MoveAxisRawInput * _MoveSpeed calculation value is.
_PlayerPhysics.CanMove = false;
ActualDash();
yield return new WaitForSeconds(DASH_DURATION);
// Disable trails.
SetActiveTrails(false);
// We can set the velocity again to be handler by the player's movement.
_PlayerPhysics.CanMove = true;
// We can dash again. This could be after another WaitForSeconds to add a little delay after a dash.
canDash = true;
}
public void ActualDash()
{
// Play SFX
if (dashSFXHandler != null)
dashSFXHandler.PlaySound();
// Activate body highlight effect
_PlayerMaterials.SetActiveHighlightBody(DASH_DURATION, intensity: 1.25f);
// Zero out rigidbody velocity to have a consistent dash
_PlayerPhysics.SetVelocity(Vector2.zero);
// AddForce towards move direction
_PlayerPhysics.AddForce(TadaInput.MoveAxisRawInput.normalized, dashForce, ForceMode2D.Impulse);
}
private void SetActiveTrails(bool value)
{
for (int i = 0; i < dashTrails.Length; i++)
{
if (dashTrails != null)
{
dashTrails[i].emitting = value;
}
}
}
}
```
- **PlayerController:** This class checks for the player's inputs using `TadaInput` and calls methods from others classes based on those inputs.
```c#
// This is how the skill Dash is called.
if (TadaInput.GetKeyDown(TadaInput.ThisKey.Dash) && _PlayerPhysics.Velocity.sqrMagnitude > 0)
_PlayerSkills.Dash();
```
---
### Utilities ([ToC↑](#home))
A utility class is one that has one or many functions that are totally independent and can be reused as is on any other project (e.g. UnityEngine.Mathf which has lots of methods that we use as is on our projects).
In this version (0.2) I've four utility classes, one that handles arrays and three to handle rotations. The first I called it `ArraysHandler` and at the moment I use it to find the next or previous index of any array.
Here is the code:
```c#
///
/// Contains methods to handle arrays.
///
public static class ArraysHandler
{
public static int GetNextIndex (int currentIndex, int arrayLength)
{
return (currentIndex + 1) % arrayLength;
}
public static int GetPreviousIndex(int currentIndex, int arrayLength)
{
return ((currentIndex - 1) + arrayLength) % arrayLength;
}
}
```
And from the three that I use for rotations, the most important one to me is the one I called `LookAt2Dv2`, which is an improved version of the one I had on my v0.1 study and the main reason why I'm now able to use a single set or arms and no duplicated weapons.
**I'll add the code here if you simply need a great 2D LookAt class and you are feeling lazy to get it from the repo.** The only thing that is worth noting from this class is that it's not entirely a utility class that you can use as is, it will try to look for `TadaInput.MouseWorldPos` if the use of a mouse as a target is selected, but that's something that can be edited quite easily (I'll make sure to convert this class into a full independent utility later on).
```c#
using UnityEngine;
///
/// New v2. The gameobject that has this component attached will instantly rotate to make its x or y axis look
/// towards the assigned target or towards mouse world position if a exposed enum is selected. The direction can be
/// inverted by checking isFlipAxis. Also there is an option to disable local update if a linked control is
/// needed. It can also use a smooth rotation by enabling isSmoothRotationEnable.
///
public class LookAt2Dv2 : MonoBehaviour
{
// --------------------------------------
// ----- 2D Isometric Shooter Study -----
// ----------- by Tadadosi --------------
// --------------------------------------
// ---- Support my work by following ----
// ---- https://twitter.com/tadadosi ----
// --------------------------------------
[TextArea(4, 10)]
public string notes = "New v2. The gameobject that has this component attached will instantly rotate to make its x or y axis look " +
"towards the assigned target or towards mouse world position if a exposed enum is selected. The direction can be inverted by " +
"checking isFlipAxis. Also there is an option to disable local update if a linked control is needed. It can also use a " +
"smooth rotation by enabling isSmoothRotationEnable.";
// TargetTransform: Look at the gameobject Transform from the public variable targetTransform.
// MouseWorldPosition: Look at the mouse world position stored by the TadaInput class.
public enum LookAtTarget { TargetTransform, MouseWorldPosition }
[SerializeField] private LookAtTarget lookAtTarget = LookAtTarget.TargetTransform;
[Tooltip("If you are using a Transform, select TargetTransform from lookAtTarget dropdown list.")]
public Transform targetTransform;
private enum Axis { X, Y }
[SerializeField] private Axis axis = Axis.Y;
[Tooltip("Used when isSmoothRotationEnable is true.")]
[SerializeField] private float turnRate = 10f;
[Tooltip("Use to set an initial offset angle or use SetOffsetAngle method to do it via code.")]
[SerializeField] private float offsetLookAtAngle = 0f;
[Tooltip("e.g. writing 30 will make the axis have a range of -30 to 30 degrees.")]
[SerializeField] private float maxAngle = 360f;
[Tooltip("Check to let this behaviour be run by the local Update() method and Uncheck if you want to call it from any other class by using UpdateLookAt().")]
[SerializeField] private bool isUpdateCalledLocally = false;
[Tooltip("Check to smoothly rotate towards target rotation using turnRate as variable.")]
public bool isSmoothRotationEnable = false;
[Tooltip("Check to flip the axis and use the negative side to look at")]
public bool isFlipAxis = false;
[Header("Debug")]
[SerializeField] private Color debugColor = Color.green;
[SerializeField] private bool debug = false;
private Vector3 targetPosition;
private Vector3 direction;
private Vector3 upwardAxis;
private void Update()
{
if (!isUpdateCalledLocally)
return;
UpdateLookAt();
}
public void UpdateLookAt()
{
Vector3 myPosition = transform.position;
if (lookAtTarget == LookAtTarget.MouseWorldPosition)
targetPosition = TadaInput.MouseWorldPos;
else if ((lookAtTarget == LookAtTarget.TargetTransform))
{
if (targetTransform == null)
{
Debug.LogError(gameObject.name + " target missing!");
return;
}
targetPosition = targetTransform.position;
}
// Ensure there is no 3D rotation by aligning Z position
targetPosition.z = myPosition.z;
// Vector from this object towards the target position
direction = (targetPosition - myPosition).normalized;
switch (axis)
{
case Axis.X:
if (!isFlipAxis)
{
// Rotate direction by 90 degrees around the Z axis
upwardAxis = Quaternion.Euler(0, 0, 90 + offsetLookAtAngle) * direction;
}
else
{
// Rotate direction by -90 degrees around the Z axis
upwardAxis = Quaternion.Euler(0, 0, -90 + offsetLookAtAngle) * direction;
}
break;
case Axis.Y:
if (!isFlipAxis)
upwardAxis = direction;
else
upwardAxis = -direction;
break;
default:
break;
}
// Get the rotation that points the Z axis forward, and the X or Y axis 90° away from the target
// (resulting in the Y or X axis facing the target).
Quaternion targetRotation = Quaternion.LookRotation(forward: Vector3.forward, upwards: upwardAxis);
if (debug)
Debug.DrawLine(transform.position, targetPosition, debugColor);
if (!isSmoothRotationEnable)
{
// Update the rotation if it's inside the maxAngle limits.
if (Quaternion.Angle(Quaternion.identity, targetRotation) < maxAngle)
transform.rotation = targetRotation;
return;
}
// Smooth rotation.
Quaternion rotation = Quaternion.Lerp(transform.rotation, targetRotation, turnRate * Time.deltaTime);
// Update the rotation if it's inside the maxAngle limits.
if (Quaternion.Angle(Quaternion.identity, rotation) < maxAngle)
transform.rotation = rotation;
}
public void SwitchToTarget(LookAtTarget target)
{
lookAtTarget = target;
}
public void SetOffsetAngle(float value)
{
offsetLookAtAngle = value;
}
}
```
---
### Weapons ([ToC↑](#home))
The weapon system has two main classes, the first one is called `Weapon` and it has four virtual methods that can be overriden in a new derived class to create any kind of weapons. It also has a bool called `canUse` which is used along with a float called `_UseRate` to control the speed in which the player can call an action on the current weapon. Having a base class like this one is great, it lets you have N amount of derived weapons that can be used by simply storing the class `Weapon` in a property and calling its base methods.
In order to call the base methods and handle N number of derived weapons, I made the second class called `WeaponHandler`. The way it works is that it has a `Weapon[]` array which is used throughout the class to do actions like `SwitchWeapon`, `SwitchUseRate` and `UseWeapon`.
> I believe that these two classes turned out great and can be reused on other projects with just a little editing or by also importing the file `ArraysHandler` because some of the methods need that utility to work.
To make the two weapons that are currently on this project, I created the derived class called `Weapon_ShootProjectileCanCharge` which has a primary action to shoot projectiles and a secondary action with a timer that after reaching its duration shoots a secondary projectile. This is what this weapon basically does:
1. When it gets enabled it spawns two projectiles (it uses instantiate) which get stored on two variables (primaryProjectile and secondaryProjectile) and also get disabled as soon as they are created.
2. If the player presses the primary action button the current weapon `PrimaryAction` method is called and does the following:
```c#
public override void PrimaryAction(bool value)
{
base.PrimaryAction(value);
// Can be executed only if there is a projectile available and canUse is true.
if (primaryProjectile != null && canUse)
{
// Play the basic animation if WeaponAnim_ShootProjectileCanCharge is available.
if (anim != null)
anim.PlayAnimation(WeaponAnim_ShootProjectileCanCharge.Animation.BasicShot);
// Make the camera Shake.
CameraShake.Shake(duration: 0.075f, shakeAmount: 0.1f, decreaseFactor: 3f);
// Enable the primary projectile.
primaryProjectile.SetActive(true);
// Call the method Fire on the primary projectile to launch it towards the crosshair direction.
primaryProjectile.Fire();
// We make it null to give room to a new instantiated projectile.
primaryProjectile = null;
// We make it false to execute the base Update actions which makes it true again after UseRate duration is reached,
// which then calls the method OnCanUse() that's used to spawn new projectiles and to call to return to the Idle anim.
canUse = false;
}
}
```
3. If the player presses the secondary action button the current weapon `SecondaryAction` method is called and does the following:
```c#
public override void SecondaryAction(bool value)
{
base.SecondaryAction(value);
// The purpose of this action is to let the player hold the secondary action button to make the bool
// isReceivingInput true, which in turn enables a timer and a series of actions to ultimately launch the
// secondary projectile.
// After firing the projectile, canUse is set to false and because the player can continuously call this method,
// we use this to stop isReceivingInput from getting a true value.
if (!canUse)
{
// Cancel inputs after use.
isReceivingInput = false;
return;
}
// We stop the code here if one of the needed variables is missing.
if ((secondaryProjectile == null || chargingPFX == null || chargingSFX == null))
{
Debug.LogWarning(gameObject.name + ": missing prefabs!");
return;
}
// We make it true if the player is pressing the secondary action button or false if not.
// When it's true, it activates the actions on the Update method of this class.
isReceivingInput = value;
}
```
3.1. When `isReceivingInput` is `true` a timer is enabled and a sequence of actions are called if certain conditions are met.
```c#
if (isReceivingInput)
{
// Execute the initial actions that take place in the first frame after isReceivingInput is set to true.
OnChargingStart();
// Timer: Increase the value of chargingTime by adding Time.deltaTime on each frame.
chargingTime += Time.deltaTime;
// Update OnCharging actions and pass chargingTime as argument.
OnCharging(chargingTime);
// If charging time is equal or greater than the constant charge duration, execute the last actions.
if (chargingTime >= CHARGE_DURATION)
OnChargingEnd();
}
```
3.2. The first action that gets called is `OnChargingStart` and it's used to do the following actions:
```c#
private void OnChargingStart()
{
// This actions can only be executed if isCharging is false.
if (!isCharging)
{
// Play the charging animation if WeaponAnim_ShootProjectileCanCharge is available.
if (anim != null)
anim.PlayAnimation(WeaponAnim_ShootProjectileCanCharge.Animation.Charging);
// We set it to true to avoid calling this method more than once.
isCharging = true;
// Make the camera Shake.
CameraShake.Shake(duration: CHARGE_DURATION, shakeAmount: 0.065f, decreaseFactor: 1f);
// Enable the charging visual effects.
chargingPFX.SetActive(true);
// Play the first sound of SoundHandlerLocal.
chargingSFX.PlaySound();
}
}
```
3.3. Then the `OnCharging` action which is simply scaling the visual effects is called and updated each frame until `chargingTime` reaches `CHARGE_DURATION`.
```c#
private void OnCharging(float t)
{
// Increase the size of the charging fx to enhance it with a feeling a anticipation.
chargingPFX.transform.localScale = Vector2.one * t;
}
```
3.4. If `chargingTime` reaches `CHARGE_DURATION`, the last method `OnChargingEnd` is called and the secondary projectile is fired. These are the actions that take place when this method is called:
```c#
private void OnChargingEnd()
{
// Play the charged shot animation if WeaponAnim_ShootProjectileCanCharge is available.
if (anim != null)
anim.PlayAnimation(WeaponAnim_ShootProjectileCanCharge.Animation.ChargedShot);
// Set it to false to allow OnChargingStart to be called again.
isCharging = false;
// Set it to false to stop the player from making isReceivingInput true if it's holding the secondary action button.
canUse = false;
// Reset the timer to allow correctly restarting the charging action.
chargingTime = 0.0f;
// Make the camera Shake by a greater value.
CameraShake.Shake(duration: 0.2f, shakeAmount: 1f, decreaseFactor: 3f);
// Enable the projectile.
secondaryProjectile.SetActive(true);
// Call the method Fire on the projectile to launch it towards the crosshair direction.
secondaryProjectile.Fire();
// Make it null to give room to a new instantiated projectile.
secondaryProjectile = null;
// Reset the scale of the charging visual fx.
chargingPFX.transform.localScale = Vector2.one;
// Disable the charge visual fx.
chargingPFX.SetActive(false);
}
```
3.5. Lastly if the secondary action is charging and the player releases the corresponding button, the method `OnChargeCancel` gets called and does the following actions to reset the weapon, allowing the player to use the charging action again right from the start:
```c#
private void OnChargeCancel()
{
// Return to Idle animation if WeaponAnim_ShootProjectileCanCharge is available.
if (anim != null)
anim.PlayAnimation(WeaponAnim_ShootProjectileCanCharge.Animation.Idle);
// Is set to false to allow OnChargingStart to be called again.
isCharging = false;
// Is set to zero to reset the timer so that it can correctly count the time again.
chargingTime = 0.0f;
// Stop the camera from shaking.
CameraShake.Shake(0f, 0f, 0f);
// Stop the charging SoundHandlerLocal sounds.
chargingSFX.StopSound();
// Reset the scale of the charging visual fx.
chargingPFX.transform.localScale = Vector2.one;
// Disable the charging visual fx.
chargingPFX.SetActive(false);
}
```
> It's important to note that using Instantiate() to shoot projectiles is a great choice for rapid prototyping and testing, but its a bad move when it comes to actually creating a good system, constantly creating new GameObjects and destroying them would do no good to the garbage collection system. A good choice would be creating a pool system that has pre-cloned prefabs which can be used without destroying them, they just get enabled when called and disabled when they are "destroyed". I had my focus on all other parts of this project that I wanted to improve, so at the I didn't have enough time to implement a method like a pooling system.
> Also it's worth noting that the charging action can be reused right after OnChargingEnd is called, which I think is not the best desirable behaviour, in the future I'll introduce a short delay after use.
---
### Projectiles ([ToC↑](#home))
This class is really simple, it has three custom methods. `SetActive(bool value)` to enable/disable the `SpriteRenderer` and the `BoxCollider2D` of the projectile. `Fire()` to launch the projectile towards a Vector3 called `travelDirection` which also detaches it from its parent (weapon) and calls Destroy on itself with a specified delay time. And lastly the method `Travel()` to actually move the projectile towards `travelDirection` multiplied by a `Speed` variable.
The projectile also uses the method `OnTriggerEnter2D (Collider2D collision)` to check if there was an impact, and if one is found, the projectile gets destroyed after instantiating a visual effect.
> The projectiles get destroyed because I'm using instantiate to spawn them, if the pooling system is made, this should be changed to simply hide the projectiles and also reset them after a certain amount of time traveling or after an impact happens.
### Shaders ([ToC↑](#home))
There are five simple shaders made with Shader Graph, the most elaborated one is the one used for the particle effects. At first I thought I could use a simple shader like this one to add to a particle system:

It works great for simple sprites, but for a particle system it doesn't. After searching for a while, I found that **particles uses the vector color of the object to actually change its color**, so without adding that part to the shader, I wouldn't be able to change the color by using the system options. After a little adjustments, this is the shader that I'm currently using for the particles:

It's basically the same, but now it adds the vertex color in the calculation and it works great with the particle systems, it also has `HDR Color` which I use to increase its intensity and make the particles glow by using the post processing fx `Bloom`.
---
That's it for this version of the project. I hope you find it useful and fun to mess around with.
---
### Support ([ToC↑](#home))
If you need any help or you found an issue that would like to talk about, reach out to me at one of the following places!
- Twitter @tadadosi
- Reddit u/tadadosi
---
[]()
---
I believe that knowledge should be free and easy to find. Not so long ago I was having a hard time figuring all this stuff out, so I started making this type of projects hoping that it will be useful to you or to anyone else who finds it.
If you would like to support my work, consider doing any of the following:
Click the image to go to itch.io and try my new game demo.
(if you like it, please remember to rate it, it's a big help.)
---
### Many thanks to
- My awesome wife that encourages me every day.
- Unity for existing and being free!
- All the people who has given me great feedback and motivation.
- All the artist / programmers who are constantly making free knowlegde available.
- And anyone who's reading this :)
- P.S.: **Extra thanks if you follow my twitter account and play my game :D!**
---
### Credits ([ToC↑](#home))
#### Scripts
- CameraShake by ftvs on Github
- Singleton pattern MIT Licence @ Unity Community
#### Sound FX (Freesound.org)
- Short Laser Shots by Emanuele_Correani - CC-BY-3.0
- Sci-Fi Force Field Impact 15 by StormwaveAudio - CC-BY-3.0
- Sci_FI_Weapon_01 by ST303 - CC0 1.0
- SciFi Gun - Mega Charge Cannon by dpren - CC0 1.0
#### Music (Freemusicarchive.org)
- Azimutez by Sci Fi Industries - CC BY-NC-SA 3.0
#### Repo readme
- sampleREADME.md by fvcproductions on Github
- Github - Creating and highlighting code blocks
---
### License
- MIT license
- The MIT License only applies to the code tagged with "by Tadadosi" and the Unity project setup in this repo, it does not include the sprites and the audio files.
- Pixel art sprites are free for personal use.