[Update] 게임형식 및 오류수정

1. 게임형태 변경
  - Game스텝에서 기존 1단계, 2단계 수정(회전만 가능하게).
  - ShootButton을 만들어 플레이어도 총알을 쏠수 있도록 수정.
  - Bullet, BulletSpawner, PlayerController를 수정
  - SpawnZone을 생성하여 기존 고정이던 스포너를 랜덤으로 생성.

2. 오류 수정
 - 모바일 버전 시 플레이어가 한방향만 보던 형상 수정.
 - 펜스에서 플레이어 캐릭터가 닿으면 회전하던 형상 수정.
This commit is contained in:
jongjae0305
2026-04-29 09:22:08 +09:00
parent ed1e24be1d
commit 8298b2559c
34 changed files with 3558 additions and 2408 deletions
+29 -29
View File
@@ -4,11 +4,16 @@ public class Bullet : MonoBehaviour
{
public float speed = 8f;
private Rigidbody rb;
public GameObject effect;
public AudioSource audioSource;
public AudioClip clip;
public bool isPlayerBullet = false;
void Start()
{
rb = GetComponent<Rigidbody>();
rb.linearVelocity = transform.forward * speed;
Destroy(gameObject, 3f);
}
private void OnTriggerEnter(Collider other)
{
@@ -19,33 +24,28 @@ public class Bullet : MonoBehaviour
if (other.CompareTag("Player"))
{
Instantiate(effect, other.transform.position, Quaternion.identity);
PlayerController pc = other.GetComponent<PlayerController>();
if (pc != null)
if (!isPlayerBullet)
{
pc.Die();
GameObject.Find("The_Lead_Hits_Deep").GetComponent<AudioSource>().Stop();
Instantiate(effect, other.transform.position, Quaternion.identity);
PlayerController pc = other.GetComponent<PlayerController>();
if (pc != null)
{
pc.Die();
GameManager.instance.PlayDeathMusic();
}
Destroy(gameObject);
}
return;
}
if (other.CompareTag("Spawner"))
{
if (isPlayerBullet)
{
Destroy(other.gameObject);
Destroy(gameObject);
}
return;
}
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
rb = GetComponent<Rigidbody>();
rb.linearVelocity = transform.forward * speed;
Destroy(gameObject, 3f);
}
// Update is called once per frame
void Update()
{
}
}
}
+32 -6
View File
@@ -17,26 +17,38 @@ public class BulletSpawner : MonoBehaviour
float rate;
float time;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
time = 0f;
rate = Random.Range(min, max);
target = FindFirstObjectByType<PlayerController>().transform;
PlayerController player = FindFirstObjectByType<PlayerController>();
if (player != null)
{
target = player.transform;
}
else
{
Debug.LogError("BulletSpawner: 플레이어를 찾을 수 없습니다! 씬에 PlayerController가 있는지 확인하세요.");
}
if (GameManager.instance != null)
{
GameManager.instance.RegisterSpawner(this);
}
}
// Update is called once per frame
void Update()
{
time += Time.deltaTime;
if (target == null) return;
time += Time.deltaTime;
transform.LookAt(target);
if(time >= rate)
if (time >= rate)
{
time = 0f;
rate = Random.Range(min, max);
GameObject obj = Instantiate(prefab, firePoint.transform.position, firePoint.transform.rotation);
@@ -46,6 +58,20 @@ public class BulletSpawner : MonoBehaviour
if (bulletScript != null)
{
bulletScript.speed = bulletSpeed;
bulletScript.isPlayerBullet = false;
}
}
}
void OnDestroy()
{
if (GameManager.instance != null)
{
GameManager.instance.UnregisterSpawner(this);
if (!GameManager.instance.isGameover)
{
GameManager.instance.RequestRespawn();
}
}
}
+107 -20
View File
@@ -1,4 +1,6 @@
using NUnit.Framework.Constraints;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
@@ -11,36 +13,41 @@ public class GameManager : MonoBehaviour
public Text recordText;
private float surviveTime;
private bool isGameover;
public bool isGameover;
public GameObject restartButton;
public void EndGame()
public static GameManager instance;
public AudioSource leadAudio;
public AudioSource deathAudio;
public List<BulletSpawner> activeSpawners = new List<BulletSpawner>();
public Transform player;
public SpawnZone spawnZone;
public GameObject spawnerPrefab;
public void RegisterSpawner(BulletSpawner s) => activeSpawners.Add(s);
public void UnregisterSpawner(BulletSpawner s) => activeSpawners.Remove(s);
public int maxSpawnerCount = 2;
public Transform rotationPlate;
void Awake()
{
isGameover = true;
gameoverText.SetActive(true);
restartButton.SetActive(true);
float bestTime = PlayerPrefs.GetFloat("BestTime");
if(surviveTime > bestTime)
{
bestTime = surviveTime;
PlayerPrefs.SetFloat("BestTime", bestTime);
}
recordText.text = "Best Time: " + (int)bestTime;
if (instance == null) instance = this;
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
surviveTime = 0;
isGameover = false;
if (leadAudio != null) leadAudio.Play();
if (deathAudio != null) deathAudio.Stop();
}
// Update is called once per frame
void Update()
{
if(!isGameover)
@@ -50,12 +57,10 @@ public class GameManager : MonoBehaviour
}
else
{
// 1. PC: R키
if (Input.GetKeyDown(KeyCode.R))
{
RestartGame();
}
// 2. 모바일/PC: 클릭 또는 터치 (화면 아무데나 눌러도 재시작)
if (Input.GetMouseButtonDown(0))
{
RestartGame();
@@ -73,4 +78,86 @@ public class GameManager : MonoBehaviour
{
UnityEngine.SceneManagement.SceneManager.LoadScene(1);
}
public void PlayDeathMusic()
{
if (leadAudio != null) leadAudio.Stop();
if (deathAudio != null) deathAudio.Play();
}
public void EndGame()
{
isGameover = true;
gameoverText.SetActive(true);
restartButton.SetActive(true);
float bestTime = PlayerPrefs.GetFloat("BestTime");
if (surviveTime > bestTime)
{
bestTime = surviveTime;
PlayerPrefs.SetFloat("BestTime", bestTime);
}
recordText.text = "Best Time: " + (int)bestTime;
}
public Vector3 GetValidSpawnPosition()
{
int maxAttempts = 10;
for (int i = 0; i < maxAttempts; i++)
{
Vector3 candidate = spawnZone.GetRandomPosition();
if (Vector3.Distance(candidate, player.position) < 5f) continue;
bool isOverlap = false;
foreach (var spawner in activeSpawners)
{
if (Vector3.Distance(candidate, spawner.transform.position) < 3f)
{
isOverlap = true;
break;
}
}
if (!isOverlap) return candidate;
}
return spawnZone.GetRandomPosition();
}
public void RequestRespawn()
{
StartCoroutine(RespawnRoutine());
}
IEnumerator RespawnRoutine()
{
yield return new WaitForSeconds(3f);
Vector3 validPos = GetValidSpawnPosition();
GameObject newSpawner = Instantiate(spawnerPrefab, validPos, Quaternion.identity);
if (rotationPlate != null)
{
newSpawner.transform.SetParent(rotationPlate);
}
}
public void UpdateMaxSpawnerCount(int newCount)
{
maxSpawnerCount = newCount;
int currentCount = activeSpawners.Count;
if (currentCount < maxSpawnerCount)
{
int needed = maxSpawnerCount - currentCount;
for (int i = 0; i < needed; i++)
{
RequestRespawn();
}
}
}
}
+24 -43
View File
@@ -2,17 +2,21 @@ using UnityEngine;
public class NewMonoBehaviourScript : MonoBehaviour
{
public GameObject[] spawners;
public Rotator rotator;
private GameManager gameManager;
private int currentStep = 0;
private int lastMaxCount = 2;
private int lastStep = -1;
void Start()
{
gameManager = FindFirstObjectByType<GameManager>();
InitializeDifficulty();
gameManager.UpdateMaxSpawnerCount(2);
UpdateStep(0);
}
void Update()
@@ -20,71 +24,48 @@ public class NewMonoBehaviourScript : MonoBehaviour
if (gameManager == null) return;
float currentTime = gameManager.GetSurviveTime();
int targetStep = 1;
if (currentTime > 30f) targetStep = 4;
else if (currentTime > 20f) targetStep = 3;
else if (currentTime > 10f) targetStep = 2;
else targetStep = 1;
int targetMax = 2;
if (currentStep != targetStep)
if (currentTime > 10f)
{
UpdateStep(targetStep);
targetMax = 2 + Mathf.FloorToInt((currentTime - 10f) / 4f);
}
}
if(targetMax != lastMaxCount)
{
lastMaxCount = targetMax;
gameManager.UpdateMaxSpawnerCount(targetMax);
}
void InitializeDifficulty()
{
foreach (var s in spawners) s.SetActive(false);
if (rotator != null) rotator.enabled = false;
UpdateStep(1);
int currentStep = (int)currentTime / 10;
if (currentStep != lastStep)
{
lastStep = currentStep;
UpdateStep(currentStep);
}
}
void UpdateStep(int step)
{
currentStep = step;
switch (step)
{
case 0:
case 1:
SetSpawnersActive(2);
if (rotator != null) rotator.enabled = false;
break;
case 2:
SetSpawnersActive(4);
if (rotator != null) rotator.enabled = false;
break;
case 3:
SetSpawnersActive(4);
if (rotator != null) rotator.enabled = true;
break;
case 4:
SetSpawnersActive(4);
if (rotator != null) rotator.enabled = true;
UpgradeBullSpeed(12f);
break;
}
}
void SetSpawnersActive(int count)
{
for(int i = 0; i < spawners.Length; i++)
{
spawners[i].SetActive(i < count);
}
}
void UpgradeBullSpeed(float newSpeed)
{
foreach (var obj in spawners)
{
if (obj == null) continue;
BulletSpawner spawner = obj.GetComponent<BulletSpawner>();
if (spawner != null) spawner.bulletSpeed = newSpeed;
default:
if (rotator != null) rotator.enabled = true;
break;
}
}
}
+4 -4
View File
@@ -2,16 +2,16 @@ using UnityEngine;
public class PlatformUIHandler : MonoBehaviour
{
public GameObject joystickUI; // 모바일 조이스틱 오브젝트
public GameObject buttonUI; // 가상 버튼 오브젝트
public GameObject joystickUI;
public GameObject buttonUI;
public GameObject shootButtonUI;
void Start()
{
// 아까 Intro에서 저장한 값을 확인
bool isMobile = GameSettings.IsMobile;
// 모바일이면 켜고, 아니면 끔
if (joystickUI != null) joystickUI.SetActive(isMobile);
if (buttonUI != null) buttonUI.SetActive(isMobile);
if (shootButtonUI != null) shootButtonUI.SetActive(isMobile);
}
}
+101 -10
View File
@@ -12,13 +12,28 @@ public class PlayerController : MonoBehaviour
public int shieldCount = 3;
private bool isShieldActive = false;
public GameObject shieldVisual;
public ShieldUIHandler shieldUI; // UI 전용 스크립트 참조
public ShieldUIHandler shieldUI;
[Header("Shooting")]
public GameObject bulletPrefab;
public Transform firePoint;
public int maxAmmo = 6;
public float reloadTime = 1.5f;
private int currentAmmo;
private bool isReloading = false;
public ShootButtonUI shootButtonUI;
private Vector3 lastMoveDirection = Vector3.forward;
void Start()
{
playerRigidbody = GetComponent<Rigidbody>();
// 초기 UI 상태 반영
currentAmmo = maxAmmo;
if (shootButtonUI != null) shootButtonUI.UpdateAmmoUI(currentAmmo, maxAmmo);
if (shieldUI != null) shieldUI.UpdateShieldUI(shieldCount);
}
@@ -26,11 +41,15 @@ public class PlayerController : MonoBehaviour
{
HandleMovement();
// PC 입력 처리 (모바일 아닐 때만)
if (!GameSettings.IsMobile && Input.GetKeyDown(KeyCode.Alpha1))
{
ActivateShieldLogic();
}
if (!GameSettings.IsMobile && Input.GetKeyDown(KeyCode.Space))
{
Shoot();
}
}
void HandleMovement()
@@ -40,7 +59,8 @@ public class PlayerController : MonoBehaviour
if (GameSettings.IsMobile && joystick != null)
{
Vector2 input = joystick.GetInput();
x = input.x; z = input.y;
x = input.x;
z = input.y;
}
else
{
@@ -48,16 +68,16 @@ public class PlayerController : MonoBehaviour
z = Input.GetAxis("Vertical");
}
playerRigidbody.linearVelocity = new Vector3(x * speed, 0f, z * speed);
playerRigidbody.linearVelocity = new Vector3(x * speed, playerRigidbody.linearVelocity.y, z * speed);
if (x != 0 || z != 0)
if (Mathf.Abs(x) > 0.1f || Mathf.Abs(z) > 0.1f)
{
transform.rotation = Quaternion.LookRotation(new Vector3(x, 0f, z));
lastMoveDirection = new Vector3(x, 0, z).normalized;
}
// 애니메이션
Animator anim = GetComponentInChildren<Animator>();
if (anim != null) anim.SetBool("isMoving", (x != 0 || z != 0));
if (anim != null) anim.SetBool("isMoving", (Mathf.Abs(x) > 0.1f || Mathf.Abs(z) > 0.1f));
}
public void ActivateShieldLogic()
@@ -73,7 +93,6 @@ public class PlayerController : MonoBehaviour
isShieldActive = true;
shieldCount--;
// UI 업데이트 위임
if (shieldUI != null) shieldUI.UpdateShieldUI(shieldCount);
if (shieldVisual != null) shieldVisual.SetActive(true);
@@ -88,4 +107,76 @@ public class PlayerController : MonoBehaviour
gameObject.SetActive(false);
FindFirstObjectByType<GameManager>()?.EndGame();
}
public void Shoot(Vector2? inputDirection = null)
{
// 리로드 중이거나 탄이 없으면 발사 불가
if (isReloading || currentAmmo <= 0)
return;
if (bulletPrefab == null || firePoint == null)
return;
// 탄 차감
currentAmmo--;
if (shootButtonUI != null) shootButtonUI.UpdateAmmoUI(currentAmmo, maxAmmo);
// 방향 결정
Vector3 lookDir;
if (inputDirection.HasValue && inputDirection.Value.magnitude > 0.1f)
{
lookDir = new Vector3(inputDirection.Value.x, 0, inputDirection.Value.y).normalized;
lastMoveDirection = lookDir;
}
else
{
lookDir = lastMoveDirection;
}
Quaternion bulletRotation = Quaternion.LookRotation(lookDir);
GameObject bullet = Instantiate(bulletPrefab, firePoint.position, bulletRotation);
bullet.transform.forward = lookDir;
Bullet bulletScript = bullet.GetComponent<Bullet>();
if (bulletScript != null)
{
bulletScript.isPlayerBullet = true;
bulletScript.speed = 8f;
}
Collider bulletCollider = bullet.GetComponent<Collider>();
Collider playerCollider = GetComponent<Collider>();
if (bulletCollider != null && playerCollider != null)
{
Physics.IgnoreCollision(bulletCollider, playerCollider);
}
// 마지막 탄을 쐈으면 자동 리로드 시작
if (currentAmmo <= 0)
{
StartCoroutine(ReloadRoutine());
}
}
private IEnumerator ReloadRoutine()
{
isReloading = true;
if (shootButtonUI != null) shootButtonUI.SetReloading(true);
yield return new WaitForSeconds(reloadTime);
currentAmmo = maxAmmo;
isReloading = false;
if (shootButtonUI != null)
{
shootButtonUI.SetReloading(false);
shootButtonUI.UpdateAmmoUI(currentAmmo, maxAmmo);
}
}
void LateUpdate()
{
transform.rotation = Quaternion.LookRotation(lastMoveDirection);
}
}
-2
View File
@@ -3,12 +3,10 @@ using TMPro;
public class RestartTextHandler : MonoBehaviour
{
// [SerializeField]를 쓰면 private이어도 인스펙터 창에 뜹니다!
[SerializeField] private TextMeshProUGUI restartText;
void OnEnable()
{
// 이제 null 체크를 할 필요도 거의 없습니다. (인스펙터에서 넣을 거니까요)
if (restartText == null)
{
Debug.LogError("인스펙터에 텍스트 오브젝트를 할당해주세요!");
-10
View File
@@ -8,15 +8,5 @@ public class SceneChange : MonoBehaviour
{
SceneManager.LoadScene("Game");
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
+2 -2
View File
@@ -3,8 +3,8 @@ using UnityEngine.UI;
public class ShieldRelocator : MonoBehaviour
{
public Transform pcAnchor; // PC 화면 위치
public Transform mobileAnchor; // 모바일 버튼 내부 위치
public Transform pcAnchor;
public Transform mobileAnchor;
void Awake()
{
-3
View File
@@ -7,19 +7,16 @@ public class ShieldUIHandler : MonoBehaviour
[SerializeField] private Image shieldIcon;
[SerializeField] private TextMeshProUGUI countText;
// UI 업데이트만 담당하는 함수
public void UpdateShieldUI(int count)
{
if (countText != null)
{
countText.text = count.ToString();
// 개수가 0이면 텍스트 숨기기
countText.gameObject.SetActive(count > 0);
}
if (shieldIcon != null)
{
// 색상 조절 로직
shieldIcon.color = (count > 0) ? Color.white : new Color(0.2f, 0.2f, 0.2f, 0.5f);
}
}
+47
View File
@@ -0,0 +1,47 @@
using UnityEngine;
using UnityEngine.EventSystems;
public class ShootButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
[SerializeField] private float rotationLimit = 40;
[SerializeField] private float rotationSpeed = 15;
private bool rotate = false;
private bool isDisabled = false;
public PlayerController playerController;
void FixedUpdate()
{
float targetRotate = rotate ? rotationLimit : 0f;
Quaternion target = Quaternion.Euler(targetRotate, 0, 0);
transform.rotation = Quaternion.Slerp(transform.rotation, target, Time.deltaTime * rotationSpeed);
}
public void OnPointerDown(PointerEventData pointerEventData)
{
if (isDisabled) return;
rotate = true;
if (playerController != null)
{
Vector2? input = (GameSettings.IsMobile && playerController.joystick != null)
? playerController.joystick.GetInput()
: null;
playerController.Shoot(input);
}
}
public void OnPointerUp(PointerEventData pointerEventData)
{
rotate = false;
}
// 외부에서 호출
public void SetDisabled(bool disabled)
{
isDisabled = disabled;
if (disabled) rotate = false;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4de305db8999c874386cdb0baaa3eb61
+42
View File
@@ -0,0 +1,42 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class ShootButtonUI : MonoBehaviour
{
[Header("References")]
public TMP_Text ammoText;
private ShootButton shootButton;
private int lastCurrent;
private int lastMax;
void Awake()
{
shootButton = GetComponent<ShootButton>();
}
public void UpdateAmmoUI(int current, int max)
{
lastCurrent = current;
lastMax = max;
if (ammoText != null)
ammoText.text = $"{current}/{max}";
}
public void SetReloading(bool reloading)
{
if (ammoText != null)
{
if (reloading)
ammoText.text = "Reload";
else
ammoText.text = $"{lastCurrent}/{lastMax}";
}
// 버튼 입력 차단/해제
if (shootButton != null)
shootButton.SetDisabled(reloading);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5179a74c586a0c242bff159b71c9b794
+16
View File
@@ -0,0 +1,16 @@
using UnityEngine;
public class SpawnZone : MonoBehaviour
{
public BoxCollider area;
public Vector3 GetRandomPosition()
{
Bounds bounds = area.bounds;
return new Vector3(
Random.Range(bounds.min.x, bounds.max.x),
bounds.center.y,
Random.Range(bounds.min.z, bounds.max.z)
);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 28cbeb060fae10746bedde0296e2585c