Update
This commit is contained in:
52
Assets/Script/AudioPool.cs
Normal file
52
Assets/Script/AudioPool.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class AudioPool : MonoBehaviour
|
||||
{
|
||||
public static AudioPool Instance;
|
||||
|
||||
[Header("Settings")]
|
||||
public int poolSize = 15;
|
||||
private List<AudioSource> pool = new List<AudioSource>();
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
// Khởi tạo pool các AudioSource
|
||||
for (int i = 0; i < poolSize; i++)
|
||||
{
|
||||
AudioSource source = gameObject.AddComponent<AudioSource>();
|
||||
source.playOnAwake = false;
|
||||
pool.Add(source);
|
||||
}
|
||||
}
|
||||
|
||||
public void PlaySound(AudioClip clip, float volume = 1f, bool randomizePitch = false)
|
||||
{
|
||||
if (clip == null) return;
|
||||
|
||||
// Tìm AudioSource đang rảnh
|
||||
AudioSource source = pool.Find(s => !s.isPlaying);
|
||||
|
||||
// Nếu tất cả đều đang bận, dùng đại cái đầu tiên
|
||||
if (source == null) source = pool[0];
|
||||
|
||||
source.clip = clip;
|
||||
source.volume = volume;
|
||||
|
||||
// Thêm một chút biến tấu cao độ để âm thanh tự nhiên hơn (không bị máy móc)
|
||||
source.pitch = randomizePitch ? Random.Range(0.9f, 1.1f) : 1f;
|
||||
|
||||
source.Play();
|
||||
}
|
||||
}
|
||||
2
Assets/Script/AudioPool.cs.meta
Normal file
2
Assets/Script/AudioPool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14171d151abe7c8439b6ca0647c2ca0b
|
||||
@@ -2,39 +2,63 @@ using UnityEngine;
|
||||
|
||||
public class BallShooter : MonoBehaviour
|
||||
{
|
||||
public GameObject ballPrefab; // Kéo prefab quả bóng vào đây
|
||||
public Transform shootPoint; // Kéo điểm ShootPoint vào đây
|
||||
public GameObject ballPrefab;
|
||||
public Transform shootPoint;
|
||||
public float shootForce = 500f;
|
||||
public float upwardForce = 200f; // Lực ném vòng cung lên trên
|
||||
public float upwardForce = 200f;
|
||||
|
||||
[Header("Audio")]
|
||||
public System.Collections.Generic.List<AudioClip> shootSounds;
|
||||
|
||||
[Header("Shooting Limit")]
|
||||
public float shootCooldown = 2f; // Thời gian chờ giữa 2 lần ném
|
||||
public float shootCooldown = 1f;
|
||||
private float nextShootTime = 0f;
|
||||
|
||||
public void ShootBall()
|
||||
{
|
||||
// Kiểm tra xem đã đến lúc được ném chưa
|
||||
if (Time.time < nextShootTime)
|
||||
{
|
||||
Debug.Log($"<color=yellow>Chờ một chút! Cần {(nextShootTime - Time.time):F1}s nữa để ném tiếp.</color>");
|
||||
return;
|
||||
}
|
||||
PerformShoot(Camera.main.transform.forward, shootForce, upwardForce);
|
||||
}
|
||||
|
||||
// Cập nhật thời gian ném tiếp theo
|
||||
public void FlickShoot(Vector2 swipeDelta, float swipeTime)
|
||||
{
|
||||
if (swipeDelta.y <= 0) return;
|
||||
|
||||
float speed = (swipeDelta.y / Screen.height) / Mathf.Max(swipeTime, 0.05f);
|
||||
float forceMultiplier = Mathf.Clamp(speed * 1.5f, 0.6f, 2.5f);
|
||||
|
||||
float horizontalShift = (swipeDelta.x / Screen.width) * 1.5f;
|
||||
Vector3 shootDirection = Camera.main.transform.forward + Camera.main.transform.right * horizontalShift;
|
||||
shootDirection.Normalize();
|
||||
|
||||
float finalForce = shootForce * forceMultiplier;
|
||||
float finalUpForce = upwardForce * (forceMultiplier * 0.8f);
|
||||
|
||||
PerformShoot(shootDirection, finalForce, finalUpForce);
|
||||
|
||||
#if UNITY_ANDROID || UNITY_IOS
|
||||
Handheld.Vibrate();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void PerformShoot(Vector3 direction, float force, float upForce)
|
||||
{
|
||||
if (Time.time < nextShootTime) return;
|
||||
nextShootTime = Time.time + shootCooldown;
|
||||
|
||||
// 1. Lấy vị trí ném: Từ Camera lùi xuống dưới một chút (giống tay người cầm bóng)
|
||||
// Lấy ngẫu nhiên 1 trong các âm thanh ném
|
||||
if (AudioPool.Instance != null && shootSounds != null && shootSounds.Count > 0)
|
||||
{
|
||||
AudioClip randomClip = shootSounds[Random.Range(0, shootSounds.Count)];
|
||||
AudioPool.Instance.PlaySound(randomClip, 0.8f, true);
|
||||
}
|
||||
|
||||
Vector3 spawnPosition = Camera.main.transform.position
|
||||
+ Camera.main.transform.forward * 0.5f
|
||||
- Camera.main.transform.up * 0.2f;
|
||||
|
||||
// 2. Tạo quả bóng
|
||||
GameObject newBall = Instantiate(ballPrefab, spawnPosition, Camera.main.transform.rotation);
|
||||
|
||||
// 3. Đảm bảo bóng không bị dính vào Image Target
|
||||
newBall.transform.SetParent(null);
|
||||
|
||||
// Gán vị trí ném vào script BouncyBall
|
||||
BouncyBall ballScript = newBall.GetComponent<BouncyBall>();
|
||||
if (ballScript != null)
|
||||
{
|
||||
@@ -44,13 +68,9 @@ public class BallShooter : MonoBehaviour
|
||||
Rigidbody rb = newBall.GetComponent<Rigidbody>();
|
||||
if (rb != null)
|
||||
{
|
||||
// 4. Lấy hướng nhìn của điện thoại
|
||||
Vector3 shootDirection = Camera.main.transform.forward;
|
||||
|
||||
// 5. Thêm lực ném (Mạnh hơn một chút để bay tới rổ trên bàn)
|
||||
rb.AddForce(shootDirection * shootForce + Vector3.up * upwardForce);
|
||||
rb.AddForce(direction * force + Vector3.up * upForce);
|
||||
}
|
||||
|
||||
Destroy(newBall, 5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
Assets/Script/FlickArea.cs
Normal file
31
Assets/Script/FlickArea.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
public class FlickArea : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
|
||||
{
|
||||
public BallShooter ballShooter;
|
||||
|
||||
private Vector2 startPos;
|
||||
private float startTime;
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
startPos = eventData.position;
|
||||
startTime = Time.time;
|
||||
}
|
||||
|
||||
public void OnPointerUp(PointerEventData eventData)
|
||||
{
|
||||
Vector2 endPos = eventData.position;
|
||||
float endTime = Time.time;
|
||||
|
||||
Vector2 swipeDelta = endPos - startPos;
|
||||
float swipeTime = endTime - startTime;
|
||||
|
||||
// Ensure swipeTime is not 0 to avoid division by zero
|
||||
if (swipeTime > 0 && ballShooter != null)
|
||||
{
|
||||
ballShooter.FlickShoot(swipeDelta, swipeTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Script/FlickArea.cs.meta
Normal file
3
Assets/Script/FlickArea.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19cfe4478f1746619c0c8df0e9c99faa
|
||||
timeCreated: 1778516848
|
||||
@@ -2,35 +2,70 @@ using UnityEngine;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class ScoreManager : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
public TextMeshProUGUI scoreText;
|
||||
public TextMeshProUGUI timerText;
|
||||
public TextMeshProUGUI winStatusText;
|
||||
public GameObject resultCanvas;
|
||||
public TextMeshProUGUI resultStatusText;
|
||||
public TextMeshProUGUI resultScoreText;
|
||||
public Button restartButton;
|
||||
|
||||
[Header("Distance Floating UI")]
|
||||
public GameObject distanceCanvas;
|
||||
public TextMeshProUGUI distanceText;
|
||||
|
||||
[Header("Audio Clips")]
|
||||
public List<AudioClip> scoreSounds; // Danh sách các âm thanh vào rổ ngẫu nhiên
|
||||
public AudioClip winSound;
|
||||
public AudioClip loseSound;
|
||||
public AudioClip buttonClickSound;
|
||||
public AudioClip flashSound;
|
||||
public AudioClip bgMusic;
|
||||
public AudioClip alarmSound;
|
||||
|
||||
[Header("Game Settings")]
|
||||
public float gameDuration = 300f;
|
||||
public float gameDuration = 60f;
|
||||
public int targetScore = 50;
|
||||
|
||||
private int currentScore = 0;
|
||||
private float timeRemaining;
|
||||
private bool isGameOver = false;
|
||||
private AudioSource musicSource;
|
||||
private bool alarmPlayed = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
currentScore = 0;
|
||||
timeRemaining = gameDuration;
|
||||
isGameOver = false;
|
||||
alarmPlayed = false;
|
||||
|
||||
musicSource = gameObject.AddComponent<AudioSource>();
|
||||
if (bgMusic != null)
|
||||
{
|
||||
musicSource.clip = bgMusic;
|
||||
musicSource.loop = true;
|
||||
musicSource.volume = 0.5f;
|
||||
musicSource.Play();
|
||||
}
|
||||
|
||||
UpdateScoreUI();
|
||||
if (distanceCanvas != null) distanceCanvas.SetActive(false);
|
||||
if (winStatusText != null) winStatusText.text = "";
|
||||
if (resultCanvas != null) resultCanvas.SetActive(false);
|
||||
|
||||
Debug.Log("<color=green>ScoreManager đã khởi tạo thành công!</color>");
|
||||
if (restartButton != null)
|
||||
{
|
||||
restartButton.onClick.RemoveAllListeners();
|
||||
restartButton.onClick.AddListener(() => {
|
||||
if (AudioPool.Instance != null) AudioPool.Instance.PlaySound(buttonClickSound);
|
||||
RestartGame();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
@@ -42,26 +77,44 @@ public class ScoreManager : MonoBehaviour
|
||||
timeRemaining -= Time.deltaTime;
|
||||
UpdateTimerUI();
|
||||
|
||||
if (currentScore >= targetScore)
|
||||
if (timeRemaining <= 10f)
|
||||
{
|
||||
WinGame();
|
||||
timerText.color = Mathf.PingPong(Time.time * 5, 1) > 0.5f ? Color.red : Color.white;
|
||||
if (!alarmPlayed && alarmSound != null)
|
||||
{
|
||||
if (AudioPool.Instance != null) AudioPool.Instance.PlaySound(alarmSound);
|
||||
alarmPlayed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GameOver();
|
||||
timeRemaining = 0;
|
||||
UpdateTimerUI();
|
||||
CheckGameEnd();
|
||||
}
|
||||
}
|
||||
|
||||
void CheckGameEnd()
|
||||
{
|
||||
if (currentScore >= targetScore)
|
||||
{
|
||||
EndGame("YOU WIN!", Color.green, winSound);
|
||||
StartCoroutine(CelebrationEffect());
|
||||
}
|
||||
else
|
||||
{
|
||||
EndGame("GAME OVER", Color.red, loseSound);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
Debug.Log("Va chạm Trigger với: " + other.gameObject.name);
|
||||
ProcessScore(other.gameObject);
|
||||
}
|
||||
|
||||
private void OnCollisionEnter(Collision collision)
|
||||
{
|
||||
Debug.Log("Va chạm Vật lý với: " + collision.gameObject.name);
|
||||
ProcessScore(collision.gameObject);
|
||||
}
|
||||
|
||||
@@ -69,50 +122,45 @@ public class ScoreManager : MonoBehaviour
|
||||
{
|
||||
if (isGameOver) return;
|
||||
|
||||
// Ưu tiên kiểm tra script BouncyBall
|
||||
BouncyBall ball = obj.GetComponent<BouncyBall>();
|
||||
|
||||
// Nếu không có script, hoặc bóng đã được tính điểm rồi thì bỏ qua
|
||||
if (ball == null || ball.isScored)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (ball == null || ball.isScored) return;
|
||||
|
||||
// Đánh dấu đã ghi điểm ngay lập tức
|
||||
ball.isScored = true;
|
||||
|
||||
// Tính khoảng cách
|
||||
#if UNITY_ANDROID || UNITY_IOS
|
||||
Handheld.Vibrate();
|
||||
#endif
|
||||
|
||||
// Chọn ngẫu nhiên âm thanh vào rổ
|
||||
if (AudioPool.Instance != null && scoreSounds != null && scoreSounds.Count > 0)
|
||||
{
|
||||
AudioClip randomClip = scoreSounds[Random.Range(0, scoreSounds.Count)];
|
||||
AudioPool.Instance.PlaySound(randomClip, 1f, true);
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(ball.shotPosition, transform.position);
|
||||
int points = CalculatePoints(distance);
|
||||
|
||||
currentScore += points;
|
||||
UpdateScoreUI();
|
||||
StartCoroutine(FlashScoreUI());
|
||||
|
||||
// Hiển thị khoảng cách trên rổ
|
||||
if (distanceCanvas != null)
|
||||
{
|
||||
StopAllCoroutines(); // Dừng các lần hiện trước đó để tránh chồng chéo
|
||||
StopAllCoroutines(); // Dùng cái này để đảm bảo reset mọi hiệu ứng cũ
|
||||
StartCoroutine(ShowDistanceUI(distance));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("distanceCanvas chưa được gán trong Inspector!");
|
||||
StartCoroutine(CelebrationEffect()); // Chỉ chạy nếu đã win
|
||||
}
|
||||
|
||||
Debug.Log($"<color=cyan>GHI ĐIỂM: {points}pt | Khoảng cách ném: {distance:F2}m | Tổng điểm: {currentScore}</color>");
|
||||
|
||||
// Vô hiệu hóa script hoặc quả bóng để không tính điểm lại
|
||||
ball.enabled = false;
|
||||
Destroy(obj, 0.5f);
|
||||
Destroy(obj, 1f);
|
||||
}
|
||||
|
||||
int CalculatePoints(float distance)
|
||||
{
|
||||
// Điều chỉnh lại logic: Nếu trong AR khoảng cách tính bằng Unity Unit có thể rất nhỏ
|
||||
// Bạn có thể cần nhân distance với một hệ số nếu tỉ lệ scale của bạn khác 1:1 mét
|
||||
if (distance >= 10f) return 3;
|
||||
if (distance >= 5f) return 2;
|
||||
if (distance >= 3f) return 1;
|
||||
if (distance >= 20f) return 3;
|
||||
if (distance >= 10f) return 2;
|
||||
if (distance >= 6f) return 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -127,7 +175,7 @@ public class ScoreManager : MonoBehaviour
|
||||
{
|
||||
int minutes = Mathf.FloorToInt(timeRemaining / 60);
|
||||
int seconds = Mathf.FloorToInt(timeRemaining % 60);
|
||||
timerText.text = string.Format("Time: {0:00}:{1:00}", minutes, seconds);
|
||||
timerText.text = string.Format("{0:00}:{1:00}", minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,27 +183,58 @@ public class ScoreManager : MonoBehaviour
|
||||
{
|
||||
if (distanceText != null) distanceText.text = $"{distance:F1}m";
|
||||
distanceCanvas.SetActive(true);
|
||||
yield return new WaitForSeconds(2.5f);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
distanceText.enabled = !distanceText.enabled;
|
||||
if (distanceText.enabled && AudioPool.Instance != null)
|
||||
AudioPool.Instance.PlaySound(flashSound, 0.7f, true);
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
}
|
||||
|
||||
distanceText.enabled = true;
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
distanceCanvas.SetActive(false);
|
||||
}
|
||||
|
||||
void WinGame()
|
||||
IEnumerator FlashScoreUI()
|
||||
{
|
||||
isGameOver = true;
|
||||
if (winStatusText != null)
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
winStatusText.text = "YOU WIN!";
|
||||
winStatusText.color = Color.green;
|
||||
scoreText.color = Color.yellow;
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
scoreText.color = Color.white;
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
void GameOver()
|
||||
IEnumerator CelebrationEffect()
|
||||
{
|
||||
isGameOver = true;
|
||||
if (winStatusText != null)
|
||||
while (isGameOver)
|
||||
{
|
||||
winStatusText.text = "GAME OVER";
|
||||
winStatusText.color = Color.red;
|
||||
resultStatusText.color = new Color(Random.value, Random.value, Random.value);
|
||||
resultScoreText.transform.localScale = Vector3.one * (1f + Mathf.PingPong(Time.time * 2, 0.2f));
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
void EndGame(string status, Color statusColor, AudioClip endClip)
|
||||
{
|
||||
isGameOver = true;
|
||||
if (musicSource != null) musicSource.Stop();
|
||||
if (AudioPool.Instance != null) AudioPool.Instance.PlaySound(endClip);
|
||||
|
||||
if (resultCanvas != null)
|
||||
{
|
||||
resultCanvas.SetActive(true);
|
||||
resultStatusText.text = status;
|
||||
resultStatusText.color = statusColor;
|
||||
resultScoreText.text = "Final Score: " + currentScore;
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartGame()
|
||||
{
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user