Isometric twin stick shooter: Shooting up and/or down

I’m currently working on an 3d isometric twin stick shooter for android. I have used the Survival shooter tutorial as a base to build upon.

I wan’t to add the ability for the player to shoot upwards/downwards, for example up or down stairs. This will allow me to add more diversity to the levels.

If I was developing for PC(keyboard+mouse) this would have been easy because I would be able to Raycast to the position of the mouse and simply shoot towards the hitpoint. The problem however is that i’m using on-screen joystick to move and rotate the player(left stick moves player, right stick rotates player and makes him shoot in that direction).
With this current setup the player will always shoot in a straight line, and won’t be able to shoot up/down.

I have no idea how to solve this issue, thus I turned towards Unity Answers hoping that someone might be able to help me.

Player shooting script just in case it’s needed:

using UnityEngine;

public class PlayerShooting : MonoBehaviour
{
    public int damagePerShot = 20;                  // The damage inflicted by each bullet.
    public float timeBetweenBullets = 0.15f;        // The time between each shot.
    public float range = 100f;                      // The distance the gun can fire.
    public bool useTouch = true;

    float timer;                                    // A timer to determine when to fire.
    Ray shootRay;                                   // A ray from the gun end forwards.
    RaycastHit shootHit;                            // A raycast hit to get information about what was hit.
    int shootableMask;                              // A layer mask so the raycast only hits things on the shootable layer.
    //ParticleSystem gunParticles;                    // Reference to the particle system.
    LineRenderer gunLine;                           // Reference to the line renderer.
    AudioSource gunAudio;                           // Reference to the audio source.
    Light gunLight;                                 // Reference to the light component.
    float effectsDisplayTime = 0.2f;                // The proportion of the timeBetweenBullets that the effects will display for.

    void Awake()
    {
        // Create a layer mask for the Shootable layer.
        shootableMask = LayerMask.GetMask("Shootable");

        // Set up the references.
        //gunParticles = GetComponent<ParticleSystem>();
        gunLine = GetComponent<LineRenderer>();
        gunAudio = GetComponent<AudioSource>();
        gunLight = GetComponent<Light>();

        #if UNITY_EDITOR
            useTouch = false;
        #endif
    }

    void Update()
    {
        // Add the time since Update was last called to the timer.
        timer += Time.deltaTime;

        // If the Fire1 button is being press and it's time to fire...
        if (!useTouch)
        {
            if (Input.GetButton("Fire1") && timer >= timeBetweenBullets)
            {
                // ... shoot the gun.
                Shoot();
            }
        }
        else
        {
            //If there is input on the right joystick and it's time to fire...
            Vector2 direction = UltimateJoystick.GetPosition("Direction");
            if ((direction.x != 0 || direction.y != 0) && timer >= timeBetweenBullets)
                Shoot();
        }

        // If the timer has exceeded the proportion of timeBetweenBullets that the effects should be displayed for...
        if (timer >= timeBetweenBullets * effectsDisplayTime)
        {
            // ... disable the effects.
            DisableEffects();
        }
    }

    public void DisableEffects()
    {
        // Disable the line renderer and the light.
        gunLine.enabled = false;
        gunLight.enabled = false;
    }

    void Shoot()
    {
        // Reset the timer.
        timer = 0f;

        // Play the gun shot audioclip.
        gunAudio.Play();

        // Enable the light.
        gunLight.enabled = true;

        // Stop the particles from playing if they were, then start the particles.
        //gunParticles.Stop();
        //gunParticles.Play();

        // Enable the line renderer and set it's first position to be the end of the gun.
        gunLine.enabled = true;
        gunLine.SetPosition(0, transform.position);

        // Set the shootRay so that it starts at the end of the gun and points forward from the barrel.
        shootRay.origin = transform.position;
        shootRay.direction = transform.forward;

        // Perform the raycast against gameobjects on the shootable layer and if it hits something...
        if (Physics.Raycast(shootRay, out shootHit, range, shootableMask))
        {
            // Try and find an EnemyHealth script on the gameobject hit.
            EnemyHealth enemyHealth = shootHit.collider.GetComponent<EnemyHealth>();

            // If the EnemyHealth component exist...
            if (enemyHealth != null)
            {
                // ... the enemy should take damage.
                enemyHealth.TakeDamage(damagePerShot, shootHit.point);
            }

            // Set the second position of the line renderer to the point the raycast hit.
            gunLine.SetPosition(1, shootHit.point);
        }
        // If the raycast didn't hit anything on the shootable layer...
        else
        {
            // ... set the second position of the line renderer to the fullest extent of the gun's range.
            gunLine.SetPosition(1, shootRay.origin + shootRay.direction * range);
        }
    }
}

That’s a tricky problem you’ve hit upon there!

The big issue is that you can no longer assume that all enemies exist in the same plain, thus it is possible for the user to shoot in a given direction in the x/z plain, but miss enemies along that line (as they may be in a different y). If all enemies are guaranteed to be in the same plain that isn’t an issue, but now that they are you’re going to have to do some clever stuff to ‘work out’ what the user actually wants to do.

The way to think of this problem is we need to make up an imaginary ‘mouse position’, as you described if you were on a PC.

I would start by first finding all the enemies that are ‘roughly’ in the x/z direction the player is firing (ignoring y). This gives you an initial idea of what the player might be intending - if there’s an enemy in that direction they’re almost certainly trying to shoot at it! If you find more than 1 enemy you might well need to do extra checks:

  • Using a ray cast, can the player actually hit one of those enemies?
  • Which enemy is closest to the ray
  • Which enemy is closest to the player

So you can work out if there’s an enemy you think the player is aiming for. If there is, then you can treat it as though the player was aiming for the y position of that enemy. Note: this isn’t auto-aim (unless you want it to be!) - you can keep your x/z direction the same, but just use the pretend ‘mouse position’ to pick a y.

If you can’t find any enemies at all that the player is probably aiming for, then you might still want it to look along the x/z direction they’re pointing and use downwards raycasts at fixed intervals along it to find out where the ground is. Then you can pick one of those ground positions to aim at (or slightly above it). Maybe you’re looking for ‘the furthest away bit of ground the player can hit’?

Lots of things to try, but the general rule here is that you’ve got a heuristic problem. You need to work out what the player wants to happen. You won’t get it perfect, but so long as you find a way to make the player never feel ‘hard done by’, it’ll feel great :slight_smile:

good luck!

-Chris