Commit 1
This commit is contained in:
71
Assets/Scripts/VFX/SlashMeshGenerator.cs
Normal file
71
Assets/Scripts/VFX/SlashMeshGenerator.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
|
||||
public class SlashMeshGenerator : MonoBehaviour
|
||||
{
|
||||
[Header("Mesh Settings")]
|
||||
public int segments = 10; // Số lượng phân đoạn (càng cao càng mượt)
|
||||
public float length = 5f; // Chiều dài vệt chém
|
||||
public float width = 0.5f; // Chiều rộng ở giữa
|
||||
public float curviness = 1f; // Độ cong của nhát chém
|
||||
|
||||
[ContextMenu("Generate Slash Mesh")]
|
||||
public void GenerateMesh()
|
||||
{
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.name = "SukunaSlashMesh";
|
||||
|
||||
int vertexCount = (segments + 1) * 2;
|
||||
Vector3[] vertices = new Vector3[vertexCount];
|
||||
Vector2[] uvs = new Vector2[vertexCount];
|
||||
int[] triangles = new int[segments * 6];
|
||||
|
||||
for (int i = 0; i <= segments; i++)
|
||||
{
|
||||
float t = (float)i / segments; // Tiến trình từ 0 đến 1
|
||||
|
||||
// Tính toán vị trí X (chiều dài)
|
||||
float x = (t - 0.5f) * length;
|
||||
|
||||
// Tính toán độ nhọn (Width Taper): Nhỏ ở 2 đầu, to ở giữa
|
||||
// Dùng hàm Sin để tạo độ mượt hoặc (1 - |2t-1|)
|
||||
float currentWidth = Mathf.Sin(t * Mathf.PI) * width;
|
||||
|
||||
// Tính toán độ cong (Y Offset)
|
||||
float yOffset = Mathf.Pow((t - 0.5f) * 2f, 2f) * curviness;
|
||||
|
||||
// Tạo 2 đỉnh (trên và dưới) cho mỗi phân đoạn
|
||||
vertices[i * 2] = new Vector3(x, yOffset + currentWidth / 2f, 0);
|
||||
vertices[i * 2 + 1] = new Vector3(x, yOffset - currentWidth / 2f, 0);
|
||||
|
||||
// Gán UV (để Shader chạy đúng)
|
||||
uvs[i * 2] = new Vector2(t, 1);
|
||||
uvs[i * 2 + 1] = new Vector2(t, 0);
|
||||
|
||||
// Tạo tam giác (trừ phân đoạn cuối)
|
||||
if (i < segments)
|
||||
{
|
||||
int start = i * 2;
|
||||
triangles[i * 6] = start;
|
||||
triangles[i * 6 + 1] = start + 2;
|
||||
triangles[i * 6 + 2] = start + 1;
|
||||
triangles[i * 6 + 3] = start + 1;
|
||||
triangles[i * 6 + 4] = start + 2;
|
||||
triangles[i * 6 + 5] = start + 3;
|
||||
}
|
||||
}
|
||||
|
||||
mesh.vertices = vertices;
|
||||
mesh.uv = uvs;
|
||||
mesh.triangles = triangles;
|
||||
mesh.RecalculateNormals();
|
||||
mesh.RecalculateBounds();
|
||||
|
||||
GetComponent<MeshFilter>().mesh = mesh;
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
GenerateMesh();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SlashMeshGenerator.cs.meta
Normal file
2
Assets/Scripts/VFX/SlashMeshGenerator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78fcc45270373164cadbaa6681bab73b
|
||||
69
Assets/Scripts/VFX/SukunaAbilityController.cs
Normal file
69
Assets/Scripts/VFX/SukunaAbilityController.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using UnityEngine;
|
||||
using OnlyScove.Scripts;
|
||||
|
||||
public class SukunaAbilityController : MonoBehaviour
|
||||
{
|
||||
[Header("Dependencies")]
|
||||
[SerializeField] private InputReader inputReader;
|
||||
|
||||
[Header("VFX Projectiles")]
|
||||
public GameObject blackProjectilePrefab;
|
||||
public GameObject redProjectilePrefab;
|
||||
|
||||
[Header("Settings")]
|
||||
public float attackRate = 0.15f;
|
||||
public float forwardOffset = 1.5f;
|
||||
public float verticalOffset = 1.0f;
|
||||
|
||||
[Header("Random Rotation Ranges")]
|
||||
public Vector2 rangeX = new Vector2(-360f, 360f);
|
||||
public Vector2 rangeY = new Vector2(-10f, 10f);
|
||||
public Vector2 rangeZ = new Vector2(50f, 120f);
|
||||
|
||||
private float lastAttackTime = 0f;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (inputReader != null && inputReader.IsAttackHeld)
|
||||
{
|
||||
if (Time.time - lastAttackTime >= attackRate)
|
||||
{
|
||||
PerformDismantle();
|
||||
lastAttackTime = Time.time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformDismantle()
|
||||
{
|
||||
GameObject selectedPrefab = GetRandomSlashVariant();
|
||||
if (selectedPrefab == null) return;
|
||||
|
||||
// Vị trí spawn trước mặt Player
|
||||
Vector3 spawnPos = transform.position + transform.forward * forwardOffset + Vector3.up * verticalOffset;
|
||||
|
||||
// Tạo góc xoay ngẫu nhiên theo yêu cầu của bạn
|
||||
float randX = Random.Range(rangeX.x, rangeX.y);
|
||||
float randY = Random.Range(rangeY.x, rangeY.y);
|
||||
float randZ = Random.Range(rangeZ.x, rangeZ.y);
|
||||
|
||||
// Kết hợp với hướng của Player
|
||||
Quaternion spawnRot = transform.rotation * Quaternion.Euler(randX, randY, randZ);
|
||||
|
||||
// Tạo đạn
|
||||
GameObject projectile = Instantiate(selectedPrefab, spawnPos, spawnRot);
|
||||
|
||||
// Bắt đạn bay về phía trước (hướng nhìn của Player)
|
||||
if (projectile.TryGetComponent<SukunaProjectile>(out var projScript))
|
||||
{
|
||||
projScript.SetDirection(transform.forward);
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject GetRandomSlashVariant()
|
||||
{
|
||||
float chance = Random.Range(0f, 100f);
|
||||
if (chance <= 20f) return redProjectilePrefab != null ? redProjectilePrefab : blackProjectilePrefab;
|
||||
return blackProjectilePrefab;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SukunaAbilityController.cs.meta
Normal file
2
Assets/Scripts/VFX/SukunaAbilityController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1630760c9d97a5f4eb1bc179549c95cd
|
||||
229
Assets/Scripts/VFX/SukunaDomainController.cs
Normal file
229
Assets/Scripts/VFX/SukunaDomainController.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OnlyScove.Scripts
|
||||
{
|
||||
public class SukunaDomainController : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
public PlayerStateMachine playerStateMachine;
|
||||
public GameObject slashPrefab;
|
||||
public GameObject shrinePrefab;
|
||||
public VolumeProfile domainVolumeProfile;
|
||||
public Transform cinematicCameraPoint;
|
||||
|
||||
[Header("Domain Settings")]
|
||||
public float domainRadius = 15f;
|
||||
public float domainDuration = 10f;
|
||||
|
||||
[Tooltip("Số lượng vệt chém tạo ra mỗi giây")]
|
||||
public float slashRate = 15f;
|
||||
|
||||
public float shrineRiseHeight = 7f; // Độ sâu bắt đầu của miếu
|
||||
|
||||
[Tooltip("Khoảng cách từ Pivot của Miếu đến mặt sàn nơi Player đứng")]
|
||||
public float shrineFloorOffset = 0.5f;
|
||||
|
||||
public float camMoveSpeed = 4f;
|
||||
|
||||
private bool isActive = false;
|
||||
private List<GameObject> activeSlashes = new List<GameObject>();
|
||||
private Volume localVolume;
|
||||
private GameObject spawnedShrine;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (playerStateMachine == null)
|
||||
playerStateMachine = GetComponent<PlayerStateMachine>();
|
||||
|
||||
if (playerStateMachine != null && playerStateMachine.Input != null)
|
||||
{
|
||||
playerStateMachine.Input.OnPreviousInteractEvent += HandleDomainExpansion;
|
||||
Debug.Log("<color=green>[Sukuna] Sẵn sàng. slashRate: " + slashRate + "</color>");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (playerStateMachine != null && playerStateMachine.Input != null)
|
||||
{
|
||||
playerStateMachine.Input.OnPreviousInteractEvent -= HandleDomainExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDomainExpansion()
|
||||
{
|
||||
if (isActive) return;
|
||||
Debug.Log("<color=red>[Sukuna] RYŌIKI TENKAI: FUKUMA MIZUZUSHI!</color>");
|
||||
StartCoroutine(DomainSequence());
|
||||
}
|
||||
|
||||
private IEnumerator DomainSequence()
|
||||
{
|
||||
isActive = true;
|
||||
|
||||
// Lưu vị trí ban đầu của Player (Vị trí thực tế trên mặt đất)
|
||||
Vector3 playerStartPos = playerStateMachine.transform.position;
|
||||
playerStateMachine.SetControl(false);
|
||||
|
||||
CameraController camController = playerStateMachine.Cam;
|
||||
bool originalCamEnabled = true;
|
||||
Transform mainCam = Camera.main.transform;
|
||||
|
||||
if (camController != null)
|
||||
{
|
||||
originalCamEnabled = camController.enabled;
|
||||
camController.enabled = false;
|
||||
}
|
||||
|
||||
// 1. Tạo Volume (Bóng bong lãnh địa)
|
||||
GameObject volumeObj = new GameObject("SukunaDomainVolume");
|
||||
volumeObj.transform.position = playerStartPos;
|
||||
localVolume = volumeObj.AddComponent<Volume>();
|
||||
localVolume.isGlobal = false;
|
||||
localVolume.priority = 100;
|
||||
localVolume.profile = domainVolumeProfile;
|
||||
SphereCollider volumeCollider = volumeObj.AddComponent<SphereCollider>();
|
||||
volumeCollider.isTrigger = true;
|
||||
volumeCollider.radius = 0.1f;
|
||||
|
||||
// 2. Mọc miếu và đẩy Player
|
||||
if (shrinePrefab != null)
|
||||
{
|
||||
// Spawn miếu ở vị trí rất sâu dưới chân Player
|
||||
Vector3 shrineSpawnPos = playerStartPos - Vector3.up * shrineRiseHeight;
|
||||
spawnedShrine = Instantiate(shrinePrefab, shrineSpawnPos, playerStateMachine.transform.rotation);
|
||||
|
||||
float riseDuration = 2.0f;
|
||||
float elapsed = 0;
|
||||
while (elapsed < riseDuration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.SmoothStep(0, 1, elapsed / riseDuration);
|
||||
|
||||
// Di chuyển miếu lên dần dần
|
||||
Vector3 currentShrinePos = Vector3.Lerp(shrineSpawnPos, playerStartPos, t);
|
||||
spawnedShrine.transform.position = currentShrinePos;
|
||||
|
||||
// Logic đẩy Player:
|
||||
// floorY là độ cao mặt sàn của miếu tại khung hình hiện tại
|
||||
float floorY = currentShrinePos.y + shrineFloorOffset;
|
||||
|
||||
// Nếu mặt sàn của miếu đã trồi lên cao hơn vị trí chân Player ban đầu
|
||||
if (floorY > playerStartPos.y)
|
||||
{
|
||||
// Player đi theo miếu
|
||||
playerStateMachine.transform.position = new Vector3(playerStartPos.x, floorY, playerStartPos.z);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Player đứng yên trên mặt đất ban đầu, chờ miếu trồi lên đỡ
|
||||
playerStateMachine.transform.position = playerStartPos;
|
||||
}
|
||||
|
||||
// Mở rộng bán kính Volume
|
||||
volumeCollider.radius = Mathf.Lerp(0.1f, domainRadius, t);
|
||||
|
||||
// Lia Camera mượt mà
|
||||
if (cinematicCameraPoint != null)
|
||||
{
|
||||
mainCam.position = Vector3.Lerp(mainCam.position, cinematicCameraPoint.position, Time.deltaTime * camMoveSpeed);
|
||||
mainCam.rotation = Quaternion.Slerp(mainCam.rotation, Quaternion.LookRotation((playerStateMachine.transform.position + Vector3.up * 2f) - mainCam.position), Time.deltaTime * camMoveSpeed);
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Thực thi chém liên tục dựa trên slashRate
|
||||
float timer = 0;
|
||||
float slashCooldown = 1f / slashRate;
|
||||
float lastSlashTime = 0;
|
||||
|
||||
while (timer < domainDuration)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
|
||||
if (timer - lastSlashTime >= slashCooldown)
|
||||
{
|
||||
SpawnRandomSlash(playerStateMachine.transform.position);
|
||||
lastSlashTime = timer;
|
||||
}
|
||||
|
||||
// Camera luôn theo dõi Player trên đỉnh miếu
|
||||
if (cinematicCameraPoint != null)
|
||||
{
|
||||
mainCam.position = Vector3.Lerp(mainCam.position, cinematicCameraPoint.position, Time.deltaTime * camMoveSpeed * 0.5f);
|
||||
mainCam.LookAt(playerStateMachine.transform.position + Vector3.up * 2f);
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 4. Miếu sụp xuống (Player đứng lại vị trí Y ban đầu)
|
||||
if (spawnedShrine != null)
|
||||
{
|
||||
float sinkTime = 1f;
|
||||
float elapsed = 0;
|
||||
Vector3 currentShrinePos = spawnedShrine.transform.position;
|
||||
Vector3 targetSinkPos = currentShrinePos - Vector3.up * shrineRiseHeight;
|
||||
while (elapsed < sinkTime)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = elapsed / sinkTime;
|
||||
spawnedShrine.transform.position = Vector3.Lerp(currentShrinePos, targetSinkPos, t);
|
||||
|
||||
// Player từ từ hạ xuống sàn ban đầu
|
||||
float floorY = spawnedShrine.transform.position.y + shrineFloorOffset;
|
||||
if (floorY > playerStartPos.y)
|
||||
playerStateMachine.transform.position = new Vector3(playerStartPos.x, floorY, playerStartPos.z);
|
||||
else
|
||||
playerStateMachine.transform.position = playerStartPos;
|
||||
|
||||
yield return null;
|
||||
}
|
||||
Destroy(spawnedShrine);
|
||||
}
|
||||
|
||||
// 5. Thu nhỏ Volume
|
||||
float shrinkDuration = 0.5f;
|
||||
float sElapsed = 0;
|
||||
while (sElapsed < shrinkDuration)
|
||||
{
|
||||
sElapsed += Time.deltaTime;
|
||||
volumeCollider.radius = Mathf.Lerp(domainRadius, 0.1f, sElapsed / shrinkDuration);
|
||||
yield return null;
|
||||
}
|
||||
Destroy(volumeObj);
|
||||
|
||||
// Dọn dẹp
|
||||
foreach (var s in activeSlashes) if (s != null) Destroy(s);
|
||||
activeSlashes.Clear();
|
||||
|
||||
if (camController != null) camController.enabled = originalCamEnabled;
|
||||
playerStateMachine.SetControl(true);
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
private void SpawnRandomSlash(Vector3 center)
|
||||
{
|
||||
Vector2 randCircle = Random.insideUnitCircle * domainRadius;
|
||||
Vector3 spawnPos = center + new Vector3(randCircle.x, Random.Range(1f, 6f), randCircle.y);
|
||||
|
||||
if (slashPrefab != null)
|
||||
{
|
||||
GameObject slash = Instantiate(slashPrefab, spawnPos, Random.rotation);
|
||||
slash.transform.localScale *= Random.Range(0.6f, 2.5f);
|
||||
activeSlashes.Add(slash);
|
||||
StartCoroutine(DestroySlashAfterTime(slash, 0.7f));
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DestroySlashAfterTime(GameObject slash, float time)
|
||||
{
|
||||
yield return new WaitForSeconds(time);
|
||||
if (activeSlashes != null && activeSlashes.Contains(slash)) activeSlashes.Remove(slash);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SukunaDomainController.cs.meta
Normal file
2
Assets/Scripts/VFX/SukunaDomainController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 271dd39a46bad974485107bb1a070e0a
|
||||
27
Assets/Scripts/VFX/SukunaProjectile.cs
Normal file
27
Assets/Scripts/VFX/SukunaProjectile.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class SukunaProjectile : MonoBehaviour
|
||||
{
|
||||
[Header("Movement")]
|
||||
public float speed = 50f;
|
||||
public float lifetime = 3f;
|
||||
|
||||
private Vector3 moveDirection;
|
||||
|
||||
public void SetDirection(Vector3 direction)
|
||||
{
|
||||
// Nhận hướng bay từ Player (luôn là hướng phía trước)
|
||||
moveDirection = direction.normalized;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
Destroy(gameObject, lifetime);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Di chuyển đạn theo hướng đã gán, bất kể góc xoay hiển thị của nó là gì
|
||||
transform.position += moveDirection * speed * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SukunaProjectile.cs.meta
Normal file
2
Assets/Scripts/VFX/SukunaProjectile.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9b0aad9f1697954a9f8710b4e8f3f2e
|
||||
53
Assets/Scripts/VFX/SukunaSlashEffect.cs
Normal file
53
Assets/Scripts/VFX/SukunaSlashEffect.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class SukunaSlashEffect : MonoBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
public float duration = 0.2f;
|
||||
public float maxScale = 5f;
|
||||
public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
[Header("Visuals")]
|
||||
private MeshRenderer meshRenderer;
|
||||
private MaterialPropertyBlock propBlock;
|
||||
private float timer = 0f;
|
||||
private static readonly int DissolveId = Shader.PropertyToID("_Dissolve");
|
||||
|
||||
void Awake()
|
||||
{
|
||||
meshRenderer = GetComponent<MeshRenderer>();
|
||||
propBlock = new MaterialPropertyBlock();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Reset scale ban đầu
|
||||
transform.localScale = new Vector3(maxScale, 0.1f, 0.1f);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
float normalizedTime = timer / duration;
|
||||
|
||||
if (normalizedTime <= 1.0f)
|
||||
{
|
||||
// Mở rộng vệt chém theo chiều ngang (Y hoặc Z tùy mesh)
|
||||
float currentScaleY = scaleCurve.Evaluate(normalizedTime) * maxScale;
|
||||
transform.localScale = new Vector3(maxScale, currentScaleY, 1f);
|
||||
|
||||
// Điều khiển Shader bằng MaterialPropertyBlock
|
||||
if (meshRenderer != null)
|
||||
{
|
||||
meshRenderer.GetPropertyBlock(propBlock);
|
||||
propBlock.SetFloat(DissolveId, normalizedTime);
|
||||
meshRenderer.SetPropertyBlock(propBlock);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Tự hủy sau khi xong
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/VFX/SukunaSlashEffect.cs.meta
Normal file
2
Assets/Scripts/VFX/SukunaSlashEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66ad11f71e7aac841be73f4b03cf0d83
|
||||
Reference in New Issue
Block a user