// GDnD wiki example // Demonstrates: twin-stick control — move on one set of keys, aim/fire on another — plus respawn invulnerability // Related pages: [[overview-arcade-classics-as-learning-projects]], [[unity-input]], [[unity-input-system]] using UnityEngine; // The defining idea of Robotron: movement and aiming are independent. WASD moves // the character; the arrow keys fire in their own direction (with the mouse as a // fallback aim). That decoupling is what twin-stick means — see [[unity-input-system]] // for doing the same with a gamepad's two analogue sticks. public class TwinStickPlayer : MonoBehaviour { [SerializeField] private float moveSpeed = 7f; [Header("Firing")] [SerializeField] private Bullet bulletPrefab; [SerializeField] private float bulletSpeed = 14f; [SerializeField] private float fireCooldown = 0.12f; [SerializeField] private LayerMask enemyMask; private RobotronGame game; private Camera cachedCamera; private float cooldownTimer; private float invulnerableTimer; private float halfSize; public bool IsInvulnerable => invulnerableTimer > 0f; private void Awake() { cachedCamera = Camera.main; SpriteRenderer sprite = GetComponent(); halfSize = sprite != null ? sprite.bounds.extents.x : 0.25f; } public void Bind(RobotronGame owner) { game = owner; } public void Respawn(float invulnerableSeconds) { transform.position = Vector3.zero; invulnerableTimer = invulnerableSeconds; } private void Update() { if (invulnerableTimer > 0f) { invulnerableTimer -= Time.deltaTime; } Move(); AimAndFire(); } private void Move() { // Movement keys only — deliberately NOT the arrow keys. Vector2 move = Vector2.zero; if (Input.GetKey(KeyCode.W)) move.y += 1f; if (Input.GetKey(KeyCode.S)) move.y -= 1f; if (Input.GetKey(KeyCode.D)) move.x += 1f; if (Input.GetKey(KeyCode.A)) move.x -= 1f; Vector3 position = transform.position + (Vector3)(move.normalized * (moveSpeed * Time.deltaTime)); float halfHeight = cachedCamera.orthographicSize - halfSize; float halfWidth = cachedCamera.orthographicSize * cachedCamera.aspect - halfSize; position.x = Mathf.Clamp(position.x, -halfWidth, halfWidth); position.y = Mathf.Clamp(position.y, -halfHeight, halfHeight); transform.position = position; } private void AimAndFire() { cooldownTimer -= Time.deltaTime; Vector2 aim = ReadAimDirection(); if (aim == Vector2.zero || cooldownTimer > 0f || bulletPrefab == null) { return; } cooldownTimer = fireCooldown; Bullet bullet = Instantiate(bulletPrefab, transform.position, Quaternion.identity); bullet.Launch(aim, bulletSpeed, enemyMask); } private Vector2 ReadAimDirection() { // Primary: arrow keys form the second "stick". Vector2 aim = Vector2.zero; if (Input.GetKey(KeyCode.UpArrow)) aim.y += 1f; if (Input.GetKey(KeyCode.DownArrow)) aim.y -= 1f; if (Input.GetKey(KeyCode.RightArrow)) aim.x += 1f; if (Input.GetKey(KeyCode.LeftArrow)) aim.x -= 1f; if (aim != Vector2.zero) { return aim.normalized; } // Fallback: hold the mouse button to fire toward the cursor. if (Input.GetMouseButton(0)) { Vector3 mouse = cachedCamera.ScreenToWorldPoint(Input.mousePosition); return ((Vector2)(mouse - transform.position)).normalized; } return Vector2.zero; } }