Laser Defender for mobile

Hi there,

I’ve finished the 2D course for Unity and now following the mobile game course. Basically I wanted to create a mobile version of my Laser Defender game. I’ve managed to add a health pickup system for the game with some help obviously :slight_smile:

But now I am facing some issues with the new controller for my player. I am sharing my updated player class here for the mobile

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

public class Player : MonoBehaviour
{
    [Header("General")]
    [SerializeField] float moveSpeed = 5f;
    [SerializeField] float touchThreshold = 50f;

    [Header("Padding")]
    [SerializeField] float paddingLeft;
    [SerializeField] float paddingRight;
    [SerializeField] float paddingTop;
    [SerializeField] float paddingBottom;

    Vector2 minBounds;
    Vector2 maxBounds;

    Shooter shooter;

    private Vector2 touchStartPosition;
    private bool isTouchMoving = false;

    void Awake()
    {
        shooter = GetComponent<Shooter>();
    }

    void Start()
    {
        InitBounds();
    }

    void Update()
    {
        HandleInput();
        Move();
    }

    void InitBounds()
    {
        Camera mainCamera = Camera.main;
        minBounds = mainCamera.ViewportToWorldPoint(new Vector2(0, 0));
        maxBounds = mainCamera.ViewportToWorldPoint(new Vector2(1, 1));
    }

    void HandleInput()
    {
        if (Input.touchCount > 0)
        {
            foreach (Touch touch in Input.touches)
            {
                if (touch.phase == UnityEngine.TouchPhase.Began) // Fully qualify the enum
                {
                    if (IsTouchOnPlayer(touch.position))
                    {
                        touchStartPosition = touch.position;
                        isTouchMoving = true;
                        shooter.isFiring = true;
                    }
                }
                else if (touch.phase == UnityEngine.TouchPhase.Ended || touch.phase == UnityEngine.TouchPhase.Canceled) // Fully qualify the enum
                {
                    isTouchMoving = false;
                    shooter.isFiring = false;
                }
            }
        }
        else
        {
            isTouchMoving = false;
            shooter.isFiring = false;
        }
    }

    bool IsTouchOnPlayer(Vector2 touchPosition)
    {
        Vector2 screenPosition = Camera.main.WorldToScreenPoint(transform.position);
        float distance = Vector2.Distance(touchPosition, screenPosition);
        return distance < touchThreshold; 
    }

    void Move()
    {
        if (isTouchMoving)
        {
            Vector2 touchDelta = (Input.touches[0].position - touchStartPosition) * moveSpeed * Time.deltaTime;
            MovePlayerWithDelta(touchDelta);
            touchStartPosition = Input.touches[0].position;
        }
    }

    void MovePlayerWithDelta(Vector2 delta)
    {
        Vector3 newPos = transform.position + (Vector3)delta; // Convert Vector2 to Vector3
        newPos.x = Mathf.Clamp(newPos.x, minBounds.x + paddingLeft, maxBounds.x - paddingRight);
        newPos.y = Mathf.Clamp(newPos.y, minBounds.y + paddingBottom, maxBounds.y - paddingTop);
        transform.position = newPos;
    }

    public void Heal(int amount)
    {
        Health healthComponent = GetComponent<Health>();
        if (healthComponent != null)
        {
            healthComponent.Heal(amount);
        }
    }
}

I have a windows pc and an iphone so I’ve downloaded the Unity Remote 5 app to test the game on my iphone but the issue I am facing is that with the current setup the only thing I could tweak is the touch threshold and even with bigger numbers for this value my player jumps all over the screen.

Could you please have a look at this script and share some wisdom with me? Is my approach is correct and achievable? Or should I add a joystick UI to the game? I am trying to achieve a more intuitive and user friendly gameplay for my game :slight_smile:

Thanks in advance.

following discussion, likely trying to adapt this to mobile, too, in a few weeks

1 Like

Hello again everyone,

I wanted to provide an update on the progress of my mobile Laser Defender game. I’ve made several changes since my last post.

What’s New:

Joystick Controls:

  • Switched from touch controls to a joystick UI for better control.
  • Created a DynamicJoystick class to handle the joystick’s behavior.
  • Modified the Player class to work with the new joystick controls.

Boundaries:

  • Implemented code to keep the player within the viewport boundaries.

Here are my updated Dynamic Joystick and Player classes and a glimpse of the current gameplay:

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

public class DynamicJoystick : MonoBehaviour, IDragHandler, IPointerUpHandler, IPointerDownHandler
{
    [SerializeField] private RectTransform joystickArea;
    [SerializeField] private RectTransform joystick;
    public Vector2 JoystickPosition { get; private set; }
    public bool JoystickTouched { get; private set; }




    private void Start()
    {
        Debug.Log("DynamicJoystick Start() Called.");
    }

    public void OnDrag(PointerEventData eventData)
    {
        Vector2 localPoint;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(joystickArea, eventData.position, eventData.pressEventCamera, out localPoint);
        JoystickPosition = localPoint;
        JoystickPosition = Vector2.ClampMagnitude(JoystickPosition, 1);
        joystick.anchoredPosition = JoystickPosition * 50; // 50 is the radius of the joystick UI
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        JoystickTouched = true; // Set to true when touched
        OnDrag(eventData);
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        JoystickTouched = false; // Set to false when not touched
        JoystickPosition = Vector2.zero;
        joystick.anchoredPosition = Vector2.zero;
    }


    public Vector2 GetJoystickPosition()
    {
        return JoystickPosition;
    }
}

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

public class Player : MonoBehaviour
{
    [Header("General")]
    [SerializeField] float moveSpeed = 5f;
    [SerializeField] float sensitivity = 1f;
    [SerializeField] float smoothing = 0.1f;
    [SerializeField] float paddingLeft = 0f;
    [SerializeField] float paddingRight = 0f;
    [SerializeField] float paddingTop = 0f;
    [SerializeField] float paddingBottom = 0f;
    private DynamicJoystick dynamicJoystick;

    Vector2 minBounds;
    Vector2 maxBounds;

    void Start()
    {
        Debug.Log("Player Start() Called.");
        dynamicJoystick = FindObjectOfType<DynamicJoystick>();
        Debug.Log(dynamicJoystick != null ? "Dynamic Joystick found." : "Dynamic Joystick NOT found.");

        InitBounds();
    }

    void InitBounds()
    {
        Camera mainCamera = Camera.main;
        minBounds = mainCamera.ViewportToWorldPoint(new Vector2(0, 0));
        maxBounds = mainCamera.ViewportToWorldPoint(new Vector2(1, 1));
    }

    void FixedUpdate()
    {
        Move();
    }

    void Move()
    {
        if (dynamicJoystick.JoystickTouched)
        {
            Vector2 joystickPosition = dynamicJoystick.GetJoystickPosition();

            float deltaX = joystickPosition.x * moveSpeed * Time.deltaTime;
            float deltaY = joystickPosition.y * moveSpeed * Time.deltaTime;

            // Apply sensitivity
            deltaX *= sensitivity;
            deltaY *= sensitivity;

            // Calculate new positions with boundaries
            float newXPos = Mathf.Clamp(transform.position.x + deltaX, minBounds.x + paddingLeft, maxBounds.x - paddingRight);
            float newYPos = Mathf.Clamp(transform.position.y + deltaY, minBounds.y + paddingBottom, maxBounds.y - paddingTop);

            // Apply smoothing
            float smoothedX = Mathf.Lerp(transform.position.x, newXPos, smoothing * Time.deltaTime);
            float smoothedY = Mathf.Lerp(transform.position.y, newYPos, smoothing * Time.deltaTime);

            transform.position = new Vector2(smoothedX, smoothedY);
        }
    }

}

Current Issues:

  • Although the joystick controls work, they are not as smooth or intuitive as I’d like them to be, especially on an actual mobile device.

Seeking Advice:

  • How can I make the joystick more responsive and natural-feeling?
  • Are there Unity components or packages that could help me achieve better control?

I would appreciate any advice or suggestions to improve these issues. Thank you for your time!

Hello everyone,

I’m back with an update on my mobile Laser Defender game. Since my last post, I’ve made significant progress but hit a snag with the shooting mechanic. I could really use some advice!

What’s New:

  • Implemented joystick controls using AssetDynamicJoystick from the Unity Asset Store.
  • Updated the Player class to work with the new joystick controls.
  • Added touch controls for shooting.

The Issue:

My shooting mechanism works perfectly fine when I’m connected through Unity Remote 5. However, it fails to function in the mobile build. The character moves as expected, but no shooting occurs.

Unity Version:

Recently switched back to Unity 2022.3.6f1.

Error Messages:

No error messages are displayed in the Unity console.

What I’ve Tried:

  • Checked the Unity console logs; all seems fine.
  • Debugged the Shooter and Player classes; they indicate that projectiles are being created and destroyed as expected.
  • Built the project on different devices; the issue persists.
  • Cleared the Unity cache and re-built the project; no luck.

** Seeking Advice:**

  1. What could be causing the shooting mechanism to not work in the mobile build?
  2. Are there specific Unity settings I should be aware of?
  3. Any other insights or suggestions would be highly appreciated.

code snippets:
player class:

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

public class Player : MonoBehaviour
{
    [Header("General")]
    [SerializeField] float moveSpeed = 5f;
    [SerializeField] float sensitivity = 1f;
    [SerializeField] float smoothing = 0.1f;
    [SerializeField] float paddingLeft = 0f;
    [SerializeField] float paddingRight = 0f;
    [SerializeField] float paddingTop = 0f;
    [SerializeField] float paddingBottom = 0f;
    private AssetDynamicJoystick assetDynamicJoystick;

    private Shooter shooter;  // Reference to Shooter script

    Vector2 minBounds;
    Vector2 maxBounds;

    void Start()
    {
        Debug.Log("Player Start() Called.");
        assetDynamicJoystick = FindObjectOfType<AssetDynamicJoystick>();
        Debug.Log(assetDynamicJoystick != null ? "AssetDynamic Joystick found." : "AssetDynamic Joystick NOT found.");

        shooter = GetComponent<Shooter>();  // Initialize the shooter

        InitBounds();
    }

    void Update()
    {
        HandleTouchInput();  // Handle touch input for shooting
    }

    void FixedUpdate()
    {
        Move();
    }

   
    void HandleTouchInput()
    {
        if (Input.touchCount > 0)
        {
            Debug.Log("Touch detected.");
            Touch touch = Input.GetTouch(0);
            if (touch.phase == TouchPhase.Began || touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary)
            {
                shooter.isFiring = true;
            }
            else if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
            {
                shooter.isFiring = false;
            }
        }
        else
        {
            shooter.isFiring = false;
        }
    }

    void InitBounds()
    {
        Camera mainCamera = Camera.main;
        minBounds = mainCamera.ViewportToWorldPoint(new Vector2(0, 0));
        maxBounds = mainCamera.ViewportToWorldPoint(new Vector2(1, 1));
    }

    void Move()
    {
        Vector2 joystickPosition = assetDynamicJoystick.Direction;

        float deltaX = joystickPosition.x * moveSpeed * Time.deltaTime;
        float deltaY = joystickPosition.y * moveSpeed * Time.deltaTime;

        // Apply sensitivity
        deltaX *= sensitivity;
        deltaY *= sensitivity;

        // Calculate new positions with boundaries
        float newXPos = Mathf.Clamp(transform.position.x + deltaX, minBounds.x + paddingLeft, maxBounds.x - paddingRight);
        float newYPos = Mathf.Clamp(transform.position.y + deltaY, minBounds.y + paddingBottom, maxBounds.y - paddingTop);

        // Apply smoothing
        float smoothedX = Mathf.Lerp(transform.position.x, newXPos, smoothing * Time.deltaTime);
        float smoothedY = Mathf.Lerp(transform.position.y, newYPos, smoothing * Time.deltaTime);

        transform.position = new Vector2(smoothedX, smoothedY);
    }
}

shooter class:

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

public class Shooter : MonoBehaviour
{
    [Header("General")]
    [SerializeField] GameObject projectilePrefab;
    [SerializeField] float projectileSpeed = 10f;
    [SerializeField] float projectileLifetime = 5f;
    [SerializeField] float baseFiringRate = 0.2f;
    [Header("AI")]
    [SerializeField] bool useAI;
    [SerializeField] float firingRateVariance = 0f;
    [SerializeField] float minimumFiringRate = 0.1f;

    [HideInInspector] public bool isFiring;

    Coroutine firingCoroutine;
    AudioPlayer audioPlayer;

    void Awake()
    {
        audioPlayer = FindObjectOfType<AudioPlayer>();
    }
    void Start()
    {
        if(useAI)
        {
            isFiring = true; 
        }
    }


    void Update()
    {
        Fire();
    }

    void Fire()
    {
        if (isFiring && firingCoroutine == null)
        {
            firingCoroutine = StartCoroutine(FireContinuosly());
        }
        else if (!isFiring && firingCoroutine != null)
        {
            StopCoroutine(firingCoroutine);
            firingCoroutine = null;
        }

    }



    IEnumerator FireContinuosly()
    {
        while (true)
        {
            Debug.Log("Creating projectile.");  // Debug here
            GameObject instance = Instantiate(projectilePrefab, transform.position, Quaternion.identity);

            Rigidbody2D rb = instance.GetComponent<Rigidbody2D>();
            if (rb != null)
            {
                rb.velocity = transform.up * projectileSpeed;
            }

            Debug.Log("Destroying projectile after " + projectileLifetime + " seconds.");  // Debug here
            Destroy(instance, projectileLifetime);

            float timeToNextProjectile = Random.Range(baseFiringRate - firingRateVariance, baseFiringRate + firingRateVariance);
            timeToNextProjectile = Mathf.Clamp(timeToNextProjectile, minimumFiringRate, float.MaxValue);

            audioPlayer.PlayShootingClip();

            yield return new WaitForSeconds(timeToNextProjectile);
        }
    }


}

Hi consummatumest,

I’ve noticed that nobody has replied here yet. Unfortunately, we teaching assistants are currently busy providing support that is within the scope of our courses, so we cannot help with problems that are outside the scope.

If you still need help, please feel free to ask our helpful community of students for advice over on our Discord chat server.

I’m closing this thread for now as there has not been any new answer for more than a months. If you want us to reopen the thread, please message us moderators.

Good luck with your game! :slight_smile:

Privacy & Terms