Organize custom scripts and Shared under Assets/Scripts, and delete assembly definition files
This commit is contained in:
8
Assets/Scripts/Baba_yaga/AI NPC.meta
Normal file
8
Assets/Scripts/Baba_yaga/AI NPC.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92ace9da10dc8bd49a47cbdb18f8d052
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
44
Assets/Scripts/Baba_yaga/AI NPC/BehavourTreeCore.cs
Normal file
44
Assets/Scripts/Baba_yaga/AI NPC/BehavourTreeCore.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public enum NodeState
|
||||
{
|
||||
Success, Failure, Running
|
||||
}
|
||||
|
||||
public abstract class Node
|
||||
{
|
||||
protected NodeState state;
|
||||
public NodeState State => state;
|
||||
public abstract NodeState Evaluate();
|
||||
}
|
||||
|
||||
public class Selector : Node
|
||||
{
|
||||
protected List<Node> nodes = new List<Node>(); // children nodes
|
||||
|
||||
public Selector(List<Node> nodes)
|
||||
{
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public override NodeState Evaluate()
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
switch (node.Evaluate())
|
||||
{
|
||||
case NodeState.Failure:
|
||||
continue;
|
||||
case NodeState.Success:
|
||||
state = NodeState.Success;
|
||||
return state;
|
||||
case NodeState.Running:
|
||||
state = NodeState.Running;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
state = NodeState.Failure;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/AI NPC/BehavourTreeCore.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/AI NPC/BehavourTreeCore.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05bb68bbe2862134ab45f5267ec4b6bb
|
||||
49
Assets/Scripts/Baba_yaga/AI NPC/ChatBubble.cs
Normal file
49
Assets/Scripts/Baba_yaga/AI NPC/ChatBubble.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using PrimeTween;
|
||||
|
||||
namespace Baba_yaga.UI
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.UI", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class ChatBubble : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TextMeshProUGUI textDisplay;
|
||||
[SerializeField] private CanvasGroup canvasGroup;
|
||||
[SerializeField] private RectTransform bubbleRect;
|
||||
|
||||
private Transform mainCameraTransform;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (canvasGroup != null) canvasGroup.alpha = 0;
|
||||
// gameObject.SetActive(false); // Bỏ dòng này để tránh tắt nhầm NPC gốc
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
// Tìm Camera nếu chưa có (Tránh lỗi Null nếu Camera chưa spawn hoặc bị xóa)
|
||||
if (mainCameraTransform == null)
|
||||
{
|
||||
if (Camera.main != null) mainCameraTransform = Camera.main.transform;
|
||||
else return;
|
||||
}
|
||||
|
||||
// Billboard effect
|
||||
transform.LookAt(transform.position + mainCameraTransform.rotation * Vector3.forward, mainCameraTransform.rotation * Vector3.up);
|
||||
}
|
||||
|
||||
public void Show(string text, float duration = 4f)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
textDisplay.text = text;
|
||||
|
||||
// Animation using PrimeTween
|
||||
/*PrimeTween.Sequence.Create()
|
||||
.Group(Tween.Alpha(canvasGroup, 1f, 0.3f))
|
||||
.Group(Tween.Scale(bubbleRect, Vector3.zero, Vector3.one, 0.4f, Ease.OutBack))
|
||||
.Chain(Tween.Delay(duration))
|
||||
.Chain(Tween.Alpha(canvasGroup, 0f, 0.5f))
|
||||
.OnComplete(() => gameObject.SetActive(false));*/
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/AI NPC/ChatBubble.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/AI NPC/ChatBubble.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea510cea4b9ed1547ae4725a2ded949a
|
||||
52
Assets/Scripts/Baba_yaga/AI NPC/FieldOfView.cs
Normal file
52
Assets/Scripts/Baba_yaga/AI NPC/FieldOfView.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class FieldOfView : MonoBehaviour
|
||||
{
|
||||
[Range(0, 360)]
|
||||
public float viewAngle = 90f;
|
||||
public float viewRadius = 20f;
|
||||
public LayerMask obstacleLayerMask;
|
||||
public LayerMask targetLayerMask;
|
||||
|
||||
[HideInInspector] public bool canSeePlayer = false;
|
||||
[HideInInspector] public Vector3 lastKnownPlayerPosition;
|
||||
|
||||
void Start()
|
||||
{
|
||||
StartCoroutine(FindTargetWithDelay(0.1f));
|
||||
}
|
||||
|
||||
IEnumerator FindTargetWithDelay(float delay)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
FindVisibleTargets();
|
||||
}
|
||||
}
|
||||
|
||||
private void FindVisibleTargets()
|
||||
{
|
||||
canSeePlayer = false;
|
||||
var colliders = Physics.OverlapSphere(transform.position, viewRadius, targetLayerMask);
|
||||
|
||||
for (int i = 0; i < colliders.Length; i++)
|
||||
{
|
||||
var target = colliders[i].transform;
|
||||
var direction = (target.position - transform.position).normalized;
|
||||
var angle = Vector3.Angle(transform.forward, direction);
|
||||
|
||||
if (angle < viewAngle / 2)
|
||||
{
|
||||
float distanceToTarget = Vector3.Distance(transform.position, target.position);
|
||||
if (!Physics.Raycast(transform.position, direction, distanceToTarget, obstacleLayerMask))
|
||||
{
|
||||
canSeePlayer = true;
|
||||
lastKnownPlayerPosition = target.position;
|
||||
Debug.DrawLine(transform.position, target.position, Color.blue, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/AI NPC/FieldOfView.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/AI NPC/FieldOfView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 210b37cfe4a84a34a91d0a9e58856a60
|
||||
135
Assets/Scripts/Baba_yaga/AI NPC/GeminiService.cs
Normal file
135
Assets/Scripts/Baba_yaga/AI NPC/GeminiService.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace Baba_yaga.AI
|
||||
{
|
||||
[Serializable]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class Part { public string text; }
|
||||
|
||||
[Serializable]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class Content { public Part[] parts; }
|
||||
|
||||
[Serializable]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class Candidate { public Content content; }
|
||||
|
||||
[Serializable]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class GeminiResponse { public Candidate[] candidates; }
|
||||
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.AI", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class GeminiService : MonoBehaviour
|
||||
{
|
||||
public static GeminiService Instance { get; private set; }
|
||||
private int activeRequests = 0;
|
||||
private const int MAX_CONCURRENT_REQUESTS = 5;
|
||||
|
||||
[SerializeField] private string[] apiKeys = { "YOUR_KEY_1", "YOUR_KEY_2" };
|
||||
private int currentKeyIndex = 0;
|
||||
|
||||
[SerializeField] private string geminiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent";
|
||||
private float nextRequestTime = 0f;
|
||||
|
||||
private string[] fallbackDialogues = {
|
||||
"{ \"text\": \"Nice weather, isn't it?\", \"speedMod\": 0.0, \"suspicionMod\": -5.0, \"aggressionMod\": 0.0, \"braveryMod\": 0.0, \"healthMod\": 0.0 }",
|
||||
"{ \"text\": \"Did you hear something? Probably just a rat.\", \"speedMod\": 0.0, \"suspicionMod\": 2.0, \"aggressionMod\": 0.1, \"braveryMod\": -5.0, \"healthMod\": 0.0 }",
|
||||
"{ \"text\": \"I'm so tired of this shift.\", \"speedMod\": -0.2, \"suspicionMod\": 0.0, \"aggressionMod\": -0.1, \"braveryMod\": 5.0, \"healthMod\": 0.0 }",
|
||||
"{ \"text\": \"You looks strong, I should be careful.\", \"speedMod\": 0.1, \"suspicionMod\": 5.0, \"aggressionMod\": -0.2, \"braveryMod\": 10.0, \"healthMod\": 0.0 }"
|
||||
};
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); }
|
||||
else { Destroy(gameObject); }
|
||||
}
|
||||
|
||||
private string GetNextKey()
|
||||
{
|
||||
if (apiKeys == null || apiKeys.Length == 0) return "";
|
||||
string key = apiKeys[currentKeyIndex];
|
||||
currentKeyIndex = (currentKeyIndex + 1) % apiKeys.Length;
|
||||
return key;
|
||||
}
|
||||
|
||||
public void GetResponse(string persona, string prompt, Action<string> onComplete)
|
||||
{
|
||||
if (Time.time < nextRequestTime)
|
||||
{
|
||||
Debug.LogWarning("[Gemini] API is cooling down. Using fallback.");
|
||||
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeRequests >= MAX_CONCURRENT_REQUESTS)
|
||||
{
|
||||
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
|
||||
return;
|
||||
}
|
||||
StartCoroutine(PostRequest(persona, prompt, onComplete));
|
||||
}
|
||||
|
||||
private IEnumerator PostRequest(string persona, string prompt, Action<string> onComplete)
|
||||
{
|
||||
activeRequests++;
|
||||
|
||||
string jsonInstruction = " Respond ONLY with a JSON object: { " +
|
||||
"'text': 'dialogue content', " +
|
||||
"'speedMod': 0.0 (change movement speed), " +
|
||||
"'suspicionMod': 0.0 (change suspicion level), " +
|
||||
"'aggressionMod': 0.0 (0.1 to 0.5 makes NPC shoot faster, negative makes them slower), " +
|
||||
"'braveryMod': 0.0 (positive makes them less likely to panic, negative makes them scared), " +
|
||||
"'healthMod': 0.0 (positive heals NPC, negative damages them) " +
|
||||
"}. Keep values realistic.";
|
||||
|
||||
string escapedPersona = persona.Replace("\"", "\\\"");
|
||||
string escapedPrompt = prompt.Replace("\"", "\\\"");
|
||||
|
||||
var jsonBody = $@"{{
|
||||
""systemInstruction"": {{""parts"": [{{ ""text"": ""{escapedPersona} {jsonInstruction}"" }}]}},
|
||||
""contents"": [{{""parts"": [{{ ""text"": ""{escapedPrompt}"" }}]}}],
|
||||
""generationConfig"": {{
|
||||
""maxOutputTokens"": 150,
|
||||
""temperature"": 0.8,
|
||||
""responseMimeType"": ""application/json""
|
||||
}}
|
||||
}}";
|
||||
|
||||
var requestURL = $"{geminiURL}?key={GetNextKey()}";
|
||||
|
||||
using (var request = new UnityWebRequest(requestURL, "POST"))
|
||||
{
|
||||
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
|
||||
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
var response = JsonUtility.FromJson<GeminiResponse>(request.downloadHandler.text);
|
||||
if (response?.candidates?.Length > 0 && response.candidates[0].content?.parts?.Length > 0)
|
||||
{
|
||||
onComplete?.Invoke(response.candidates[0].content.parts[0].text);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[Gemini] API Error: {request.error}");
|
||||
if (request.responseCode == 429)
|
||||
{
|
||||
nextRequestTime = Time.time + 60f;
|
||||
}
|
||||
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
|
||||
}
|
||||
}
|
||||
|
||||
activeRequests--;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/AI NPC/GeminiService.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/AI NPC/GeminiService.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a859fc8e9ec10a347a3704b6045ca7e8
|
||||
198
Assets/Scripts/Baba_yaga/AI NPC/KamikazeAI.cs
Normal file
198
Assets/Scripts/Baba_yaga/AI NPC/KamikazeAI.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
public class KamikazeAI : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
public Transform player;
|
||||
|
||||
[Header("Detection")]
|
||||
public float detectRange = 15f;
|
||||
private bool canSeePlayer = false;
|
||||
|
||||
[Header("Movement & Random Patrol")]
|
||||
public float patrolSpeed = 2.5f;
|
||||
public float chaseSpeed = 7f;
|
||||
public float patrolRadius = 12f; // Bán kính của khu vực tuần tra ngẫu nhiên
|
||||
public float patrolWaitTime = 2f; // Thời gian đứng nghỉ trước khi đổi sang điểm ngẫu nhiên mới
|
||||
|
||||
private Vector3 startPosition; // Tâm của khu vực tuần tra (Vị trí ban đầu)
|
||||
private float currentWaitTime;
|
||||
private NavMeshAgent agent;
|
||||
private bool isExploding = false;
|
||||
public Node behaviorTreeRoot;
|
||||
public GameObject explosionEffectPrefab;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
agent = GetComponent<NavMeshAgent>();
|
||||
|
||||
// Lưu lại vị trí xuất phát để làm tâm, NPC sẽ chỉ đi loay hoay quanh khu vực này
|
||||
startPosition = transform.position;
|
||||
|
||||
InitBehaviorTree();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (isExploding) return;
|
||||
|
||||
if (player == null) FindPlayer();
|
||||
else CheckVision();
|
||||
|
||||
behaviorTreeRoot?.Evaluate();
|
||||
}
|
||||
|
||||
private void FindPlayer()
|
||||
{
|
||||
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
||||
if (playerObj != null) player = playerObj.transform;
|
||||
}
|
||||
|
||||
private void CheckVision()
|
||||
{
|
||||
if (Vector3.Distance(transform.position, player.position) <= detectRange)
|
||||
canSeePlayer = true;
|
||||
else
|
||||
canSeePlayer = false;
|
||||
}
|
||||
|
||||
private void InitBehaviorTree()
|
||||
{
|
||||
var explodeSequence = new Sequence(new List<Node>
|
||||
{
|
||||
new TaskNode(CheckIsCloseEnoughToExplode),
|
||||
new TaskNode(ActionTriggerExplosion)
|
||||
});
|
||||
|
||||
var chaseSequence = new Sequence(new List<Node>
|
||||
{
|
||||
new TaskNode(CheckCanSeePlayer),
|
||||
new TaskNode(ActionChase)
|
||||
});
|
||||
|
||||
// Hành động tuần tra ngẫu nhiên
|
||||
var patrolNode = new TaskNode(ActionRandomPatrol);
|
||||
|
||||
behaviorTreeRoot = new Selector(new List<Node>
|
||||
{
|
||||
explodeSequence,
|
||||
chaseSequence,
|
||||
patrolNode
|
||||
});
|
||||
}
|
||||
|
||||
#region CONDITIONS
|
||||
|
||||
private NodeState CheckCanSeePlayer()
|
||||
{
|
||||
return canSeePlayer ? NodeState.Success : NodeState.Failure;
|
||||
}
|
||||
|
||||
private NodeState CheckIsCloseEnoughToExplode()
|
||||
{
|
||||
if (player == null) return NodeState.Failure;
|
||||
float dist = Vector3.Distance(transform.position, player.position);
|
||||
return dist <= 3f ? NodeState.Success : NodeState.Failure;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ACTIONS
|
||||
|
||||
// HÀM TUẦN TRA NGẪU NHIÊN MỚI
|
||||
private NodeState ActionRandomPatrol()
|
||||
{
|
||||
// Debug.Log("Wandering randomly...");
|
||||
agent.isStopped = false;
|
||||
agent.speed = patrolSpeed;
|
||||
|
||||
// Kiểm tra xem NPC đã đi đến điểm ngẫu nhiên hiện tại chưa
|
||||
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
|
||||
{
|
||||
currentWaitTime += Time.deltaTime;
|
||||
|
||||
// Đứng đợi hết thời gian quy định rồi mới tìm đường mới
|
||||
if (currentWaitTime >= patrolWaitTime)
|
||||
{
|
||||
// 1. Lấy một điểm ngẫu nhiên trong không gian hình cầu dựa trên bán kính
|
||||
Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
|
||||
randomDirection += startPosition; // Cộng với tâm ban đầu để giới hạn khu vực
|
||||
|
||||
NavMeshHit hit;
|
||||
// 2. Ép tọa độ ngẫu nhiên đó phải nằm TRÊN bề mặt xanh của NavMesh (tránh kẹt tường)
|
||||
// Số 1 ở cuối là Area Mask (thường là Walkable)
|
||||
if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1))
|
||||
{
|
||||
agent.SetDestination(hit.position);
|
||||
}
|
||||
|
||||
currentWaitTime = 0f; // Reset thời gian chờ
|
||||
}
|
||||
}
|
||||
|
||||
return NodeState.Running;
|
||||
}
|
||||
|
||||
private NodeState ActionChase()
|
||||
{
|
||||
if (player == null) return NodeState.Failure;
|
||||
|
||||
Debug.Log("Kamikaze is rushing you!");
|
||||
agent.isStopped = false;
|
||||
agent.speed = chaseSpeed;
|
||||
agent.SetDestination(player.position);
|
||||
|
||||
return NodeState.Running;
|
||||
}
|
||||
|
||||
private NodeState ActionTriggerExplosion()
|
||||
{
|
||||
StartCoroutine(ExplosionRoutine());
|
||||
return NodeState.Success;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EXPLOSION LOGIC
|
||||
|
||||
private IEnumerator ExplosionRoutine()
|
||||
{
|
||||
isExploding = true;
|
||||
agent.isStopped = true;
|
||||
agent.velocity = Vector3.zero;
|
||||
|
||||
Debug.Log("BOMB ARMED!");
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
if (player != null)
|
||||
{
|
||||
float distToPlayer = Vector3.Distance(transform.position, player.position);
|
||||
if (distToPlayer <= 4f)
|
||||
{
|
||||
Debug.Log("BOOM! Player took damage!");
|
||||
}
|
||||
}
|
||||
|
||||
if (explosionEffectPrefab != null)
|
||||
{
|
||||
Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity);
|
||||
}
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Vẽ vùng giới hạn tuần tra màu xanh lá cây trên Scene để bạn dễ căn chỉnh độ rộng
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
// Nếu game đang chạy thì vẽ quanh tâm startPosition, nếu chưa chạy thì vẽ quanh vị trí hiện tại
|
||||
Vector3 center = Application.isPlaying ? startPosition : transform.position;
|
||||
Gizmos.DrawWireSphere(center, patrolRadius);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/AI NPC/KamikazeAI.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/AI NPC/KamikazeAI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6008ec58fb909034abd7293b55f0d558
|
||||
47
Assets/Scripts/Baba_yaga/AI NPC/Sequence.cs
Normal file
47
Assets/Scripts/Baba_yaga/AI NPC/Sequence.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class Sequence : Node
|
||||
{
|
||||
private List<Node> nodes = new List<Node>();
|
||||
|
||||
public Sequence(List<Node> nodes)
|
||||
{
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public override NodeState Evaluate()
|
||||
{
|
||||
var isAnyChildRunning = false;
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
switch (node.Evaluate())
|
||||
{
|
||||
case NodeState.Failure:
|
||||
state = NodeState.Failure;
|
||||
return state;
|
||||
case NodeState.Success:
|
||||
continue;
|
||||
case NodeState.Running:
|
||||
isAnyChildRunning = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
state = isAnyChildRunning ? NodeState.Running : NodeState.Success;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
public class TaskNode : Node
|
||||
{
|
||||
public delegate NodeState TaskDelegate();
|
||||
private TaskDelegate action;
|
||||
|
||||
public TaskNode(TaskDelegate action)
|
||||
{
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public override NodeState Evaluate()
|
||||
{
|
||||
return action();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/AI NPC/Sequence.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/AI NPC/Sequence.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfbdb66c26ddee84199051308b223b09
|
||||
8
Assets/Scripts/Baba_yaga/Audio.meta
Normal file
8
Assets/Scripts/Baba_yaga/Audio.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f390a3d7b8c9eb49ac7d779c08ef5f5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Baba_yaga/Camera.meta
Normal file
8
Assets/Scripts/Baba_yaga/Camera.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 962e9e0d2b8d78d4fbb25fb03224f618
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Baba_yaga/Duy.meta
Normal file
8
Assets/Scripts/Baba_yaga/Duy.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42e6f48f26d671a42a9b398d02557c1f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
101
Assets/Scripts/Baba_yaga/Duy/LobbyManager.cs
Normal file
101
Assets/Scripts/Baba_yaga/Duy/LobbyManager.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
#if false
|
||||
using System.Collections.Generic;
|
||||
using Fusion;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class _LobbyManager : MonoBehaviour
|
||||
{
|
||||
public GameObject lobbyPanel;
|
||||
|
||||
|
||||
public _BasicSpawner spawner;
|
||||
|
||||
[Header("Character Selection")] public TMP_InputField playerNameInput;
|
||||
|
||||
|
||||
|
||||
[Header("Room List")] public GameObject roomListParent;
|
||||
public GameObject roomListItemPrefab;
|
||||
public TMP_InputField roomNameInput;
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
async void Start()
|
||||
{
|
||||
lobbyPanel.SetActive(false);
|
||||
|
||||
|
||||
spawner = FindFirstObjectByType<_BasicSpawner>();
|
||||
await spawner.StartLobby();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void OnNextButton()
|
||||
{
|
||||
var playerName = playerNameInput.text;
|
||||
if (string.IsNullOrEmpty(playerName))
|
||||
{
|
||||
Debug.LogWarning("Player name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
|
||||
// tạo 1 player _profile tạm thời, sau này sẽ gửi lên server để tạo player object
|
||||
var _profile = new _PlayerProfile()
|
||||
{
|
||||
Name = playerName,
|
||||
|
||||
};
|
||||
spawner.SetLocalPlayerProfile(_profile);
|
||||
// đưa lên host để tạo player object, ở đây tạm thời chỉ log ra console
|
||||
Debug.Log($"Player Name: {_profile.Name}, Class: {_profile.Role}");
|
||||
// chuyển sang lobby panel
|
||||
|
||||
lobbyPanel.SetActive(true);
|
||||
}
|
||||
|
||||
// hiển thị danh sách phòng
|
||||
public void DisplayRoomList(List<SessionInfo> sessions)
|
||||
{
|
||||
Debug.Log($"Received {sessions.Count} sessions from lobby");
|
||||
// clear danh sách cũ
|
||||
foreach (Transform child in roomListParent.transform)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
if (sessions.Count == 0) return;
|
||||
|
||||
// tạo item mới cho mỗi phòng
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
var item = Instantiate(roomListItemPrefab, roomListParent.transform);
|
||||
var text = item.GetComponentInChildren<TextMeshProUGUI>();
|
||||
text.text = $"{session.Name} ({session.PlayerCount}/{session.MaxPlayers})";
|
||||
var button = item.GetComponentInChildren<Button>();
|
||||
button.onClick.AddListener(() => OnJoinRoom(session.Name));
|
||||
item.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
async void OnJoinRoom(string sessionName)
|
||||
{
|
||||
await spawner.StartClient(sessionName);
|
||||
}
|
||||
|
||||
public async void OnCreateRoomButton()
|
||||
{
|
||||
var roomName = roomNameInput.text;
|
||||
if (string.IsNullOrEmpty(roomName))
|
||||
{
|
||||
Debug.LogWarning("Room name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
|
||||
// tạo phòng mới với tên đã nhập
|
||||
await spawner.StartHost(roomName, SceneRef.FromIndex(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
2
Assets/Scripts/Baba_yaga/Duy/LobbyManager.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Duy/LobbyManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 258164a5e282e34489a3c62c443c22f0
|
||||
8
Assets/Scripts/Baba_yaga/Game.meta
Normal file
8
Assets/Scripts/Baba_yaga/Game.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a38893b1d8574b45bce269c39824bd6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
37
Assets/Scripts/Baba_yaga/Game/EloData.cs
Normal file
37
Assets/Scripts/Baba_yaga/Game/EloData.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Fusion;
|
||||
|
||||
namespace Baba_yaga.Game
|
||||
{
|
||||
[System.Serializable]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Game", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public struct PlayerEloData
|
||||
{
|
||||
public int Rating;
|
||||
public int GamesPlayed;
|
||||
|
||||
public PlayerEloData(int rating, int gamesPlayed)
|
||||
{
|
||||
Rating = rating;
|
||||
GamesPlayed = gamesPlayed;
|
||||
}
|
||||
|
||||
public static PlayerEloData Default => new PlayerEloData(1000, 0);
|
||||
}
|
||||
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Game", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public struct EloResult : INetworkStruct
|
||||
{
|
||||
public int NewRatingA;
|
||||
public int NewRatingB;
|
||||
public int DeltaA;
|
||||
public int DeltaB;
|
||||
|
||||
public EloResult(int nA, int nB, int dA, int dB)
|
||||
{
|
||||
NewRatingA = nA;
|
||||
NewRatingB = nB;
|
||||
DeltaA = dA;
|
||||
DeltaB = dB;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Game/EloData.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Game/EloData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7dc894771ad8df46831ee15ee34fe7d
|
||||
64
Assets/Scripts/Baba_yaga/Game/EloSystem.cs
Normal file
64
Assets/Scripts/Baba_yaga/Game/EloSystem.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.Game
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure logic for Elo rating calculations.
|
||||
/// Follows the 1v1 competitive formula with dynamic K-factor.
|
||||
/// </summary>
|
||||
public static class EloSystem
|
||||
{
|
||||
public const int RATING_FLOOR = 100;
|
||||
public const int PLACEMENT_GAMES = 30;
|
||||
|
||||
public static EloResult Calculate(
|
||||
int ratingA, int ratingB,
|
||||
int gamesPlayedA, int gamesPlayedB,
|
||||
float resultA) // 1=win, 0=lose, 0.5=draw
|
||||
{
|
||||
// 1. Expected Scores
|
||||
float eA = 1f / (1f + Mathf.Pow(10f, (ratingB - ratingA) / 400f));
|
||||
float eB = 1f - eA;
|
||||
|
||||
// 2. K-Factors
|
||||
int kA = GetK(ratingA, gamesPlayedA);
|
||||
int kB = GetK(ratingB, gamesPlayedB);
|
||||
|
||||
// 3. New Ratings
|
||||
int nA = Mathf.Max(RATING_FLOOR, Mathf.RoundToInt(ratingA + kA * (resultA - eA)));
|
||||
int nB = Mathf.Max(RATING_FLOOR, Mathf.RoundToInt(ratingB + kB * ((1f - resultA) - eB)));
|
||||
|
||||
return new EloResult(nA, nB, nA - ratingA, nB - ratingB);
|
||||
}
|
||||
|
||||
private static int GetK(int rating, int gamesPlayed)
|
||||
{
|
||||
if (gamesPlayed < PLACEMENT_GAMES) return 40;
|
||||
if (rating < 1200) return 32;
|
||||
if (rating < 2000) return 24;
|
||||
return 16;
|
||||
}
|
||||
|
||||
public static string GetRank(int rating)
|
||||
{
|
||||
if (rating < 800) return "Iron";
|
||||
if (rating < 1000) return "Bronze";
|
||||
if (rating < 1200) return "Silver";
|
||||
if (rating < 1500) return "Gold";
|
||||
if (rating < 1800) return "Platinum";
|
||||
if (rating < 2100) return "Diamond";
|
||||
return "Master";
|
||||
}
|
||||
|
||||
public static string GetRankColor(int rating)
|
||||
{
|
||||
if (rating < 800) return "#8A8A8A"; // Iron
|
||||
if (rating < 1000) return "#CD7F32"; // Bronze
|
||||
if (rating < 1200) return "#C0C0C0"; // Silver
|
||||
if (rating < 1500) return "#FFD700"; // Gold
|
||||
if (rating < 1800) return "#4DC8A0"; // Platinum
|
||||
if (rating < 2100) return "#7B6EE8"; // Diamond
|
||||
return "#E84D8A"; // Master
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Game/EloSystem.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Game/EloSystem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 366843378ad652a41aeb5972186ebe18
|
||||
114
Assets/Scripts/Baba_yaga/Game/MatchEloManager.cs
Normal file
114
Assets/Scripts/Baba_yaga/Game/MatchEloManager.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Fusion;
|
||||
using Baba_yaga.Game;
|
||||
using Baba_yaga.UI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// Orchestrates the Elo calculation and persistence on the Host.
|
||||
/// Broadcasts results to all clients.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Network", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class MatchEloManager : NetworkBehaviour
|
||||
{
|
||||
[Networked] public EloResult LastMatchResult { get; set; }
|
||||
[Networked] public bool IsCalculating { get; set; }
|
||||
|
||||
private Dictionary<PlayerRef, string> _playerUsernames = new Dictionary<PlayerRef, string>();
|
||||
|
||||
// Simulating the database locally instead of Firebase
|
||||
private static Dictionary<string, PlayerEloData> _localEloDatabase = new Dictionary<string, PlayerEloData>();
|
||||
|
||||
private Task<PlayerEloData> GetLocalPlayerData(string username)
|
||||
{
|
||||
if (!_localEloDatabase.TryGetValue(username, out var data))
|
||||
{
|
||||
data = PlayerEloData.Default;
|
||||
_localEloDatabase[username] = data;
|
||||
}
|
||||
return Task.FromResult(data);
|
||||
}
|
||||
|
||||
private Task SaveLocalPlayerData(string username, PlayerEloData data)
|
||||
{
|
||||
_localEloDatabase[username] = data;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override void Spawned()
|
||||
{
|
||||
if (Object.HasStateAuthority)
|
||||
{
|
||||
// In a real scenario, you'd collect usernames as players join.
|
||||
// For now, we assume they are provided or stored in PlayerRef custom data.
|
||||
// Placeholder: Use a mock or collect from Session properties.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a player's username when they join.
|
||||
/// </summary>
|
||||
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
|
||||
public void RPC_RegisterUsername(PlayerRef player, string username)
|
||||
{
|
||||
if (!_playerUsernames.ContainsKey(player))
|
||||
{
|
||||
_playerUsernames.Add(player, username);
|
||||
Debug.Log($"[EloManager] Registered {username} for {player}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by GameManager on Host when match ends.
|
||||
/// </summary>
|
||||
public async void ProcessMatchResult(PlayerRef winner, PlayerRef loser, bool isDraw = false)
|
||||
{
|
||||
if (!Object.HasStateAuthority) return;
|
||||
|
||||
IsCalculating = true;
|
||||
|
||||
string nameA = _playerUsernames.GetValueOrDefault(winner, "Unknown_A");
|
||||
string nameB = _playerUsernames.GetValueOrDefault(loser, "Unknown_B");
|
||||
|
||||
// 1. Fetch from local simulated database
|
||||
var dataA = await GetLocalPlayerData(nameA);
|
||||
var dataB = await GetLocalPlayerData(nameB);
|
||||
|
||||
// 2. Calculate
|
||||
float resultA = isDraw ? 0.5f : 1.0f;
|
||||
var result = EloSystem.Calculate(dataA.Rating, dataB.Rating, dataA.GamesPlayed, dataB.GamesPlayed, resultA);
|
||||
|
||||
// 3. Update Data Objects
|
||||
dataA.Rating = result.NewRatingA;
|
||||
dataA.GamesPlayed++;
|
||||
|
||||
dataB.Rating = result.NewRatingB;
|
||||
dataB.GamesPlayed++;
|
||||
|
||||
// 4. Save to local simulated database
|
||||
await Task.WhenAll(
|
||||
SaveLocalPlayerData(nameA, dataA),
|
||||
SaveLocalPlayerData(nameB, dataB)
|
||||
);
|
||||
|
||||
// 5. Broadcast
|
||||
LastMatchResult = result;
|
||||
IsCalculating = false;
|
||||
|
||||
Debug.Log($"[EloManager] Match Processed. Winner: {nameA} (+{result.DeltaA}), Loser: {nameB} ({result.DeltaB})");
|
||||
|
||||
// Send RPC to show UI
|
||||
RPC_NotifyClients(result);
|
||||
}
|
||||
|
||||
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
|
||||
private void RPC_NotifyClients(EloResult result)
|
||||
{
|
||||
// This is where you'd trigger the Post-Match Rive/UI
|
||||
Debug.Log($"[Client] Received Elo Update: {result.DeltaA} / {result.DeltaB}");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Game/MatchEloManager.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Game/MatchEloManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1509b216eb9b7249bc7bb184f418a6f
|
||||
8
Assets/Scripts/Baba_yaga/GameSetup.meta
Normal file
8
Assets/Scripts/Baba_yaga/GameSetup.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b562adae77c550e4db1edcabf68b0530
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
171
Assets/Scripts/Baba_yaga/GameSetup/CharacterAutoSetup.cs
Normal file
171
Assets/Scripts/Baba_yaga/GameSetup/CharacterAutoSetup.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using UnityEngine;
|
||||
using System.Text;
|
||||
|
||||
namespace Baba_yaga.GameSetup
|
||||
{
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts.GameSetup", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class CharacterAutoSetup : MonoBehaviour
|
||||
{
|
||||
[Header("Manual Overrides (If Detection Fails)")]
|
||||
[SerializeField] private float defaultHeight = 1.8f;
|
||||
[SerializeField] private float defaultShoulderWidth = 0.4f;
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private Transform modelRoot;
|
||||
[SerializeField] private bool autoDetectOnStart = true;
|
||||
[SerializeField] private float zCenterOffset = 0.05f;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (autoDetectOnStart)
|
||||
{
|
||||
ApplyAutoSetup();
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Apply Auto Setup")]
|
||||
public void ApplyAutoSetup()
|
||||
{
|
||||
CharacterController controller = GetComponent<CharacterController>();
|
||||
// PlayerStateMachine stateMachine = GetComponent<PlayerStateMachine>();
|
||||
Animator animator = GetComponentInChildren<Animator>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"<color=#4FC3F7><b>[AUTO-SETUP REPORT]</b></color> Character: <b>{gameObject.name}</b>");
|
||||
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
||||
|
||||
// 1. HEIGHT DETECTION
|
||||
float finalHeight = defaultHeight;
|
||||
string heightMethod = "Default Fallback";
|
||||
|
||||
if (animator != null && animator.GetBoneTransform(HumanBodyBones.Head) != null)
|
||||
{
|
||||
heightMethod = "Humanoid Bones (Feet to Head)";
|
||||
Transform head = animator.GetBoneTransform(HumanBodyBones.Head);
|
||||
// We measure from the local Y=0 (feet) to the head bone and add 10% for the skull/hair
|
||||
float headHeight = transform.InverseTransformPoint(head.position).y;
|
||||
finalHeight = headHeight * 1.12f; // 12% extra for the top of the skull
|
||||
}
|
||||
else
|
||||
{
|
||||
heightMethod = "Local Mesh Bounds";
|
||||
Bounds localBounds = GetRelativeBounds();
|
||||
if (localBounds.size.y > 0) finalHeight = localBounds.size.y;
|
||||
}
|
||||
|
||||
sb.AppendLine(string.Format("<b>1. HEIGHT:</b> {0:F3}m ({1}) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, heightMethod, finalHeight));
|
||||
|
||||
// 2. RADIUS DETECTION
|
||||
float shoulderWidth = defaultShoulderWidth;
|
||||
string radiusMethod = "Default Fallback";
|
||||
|
||||
if (animator != null && animator.GetBoneTransform(HumanBodyBones.Hips) != null)
|
||||
{
|
||||
radiusMethod = "Humanoid Bones (Shoulders)";
|
||||
Transform leftArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
|
||||
Transform rightArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm);
|
||||
|
||||
if (leftArm != null && rightArm != null)
|
||||
{
|
||||
float distance = Vector3.Distance(leftArm.position, rightArm.position);
|
||||
shoulderWidth = distance * 1.2f; // Add 20% for arm/shoulder thickness
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
radiusMethod = "Bounds Width Fallback";
|
||||
Bounds b = GetRelativeBounds();
|
||||
shoulderWidth = b.size.x * 0.25f;
|
||||
}
|
||||
|
||||
float finalRadius = shoulderWidth / 2f;
|
||||
sb.AppendLine(string.Format("<b>2. RADIUS:</b> {0:F3}m ({1}) [/ 2] ➔ <color=#81C784>{2:F3}m</color>", shoulderWidth, radiusMethod, finalRadius));
|
||||
|
||||
// 3. COLLISION PRECISION
|
||||
float skinWidth = finalRadius * 0.10f;
|
||||
sb.AppendLine(string.Format("<b>3. SKIN WIDTH:</b> {0:F3}m (Radius) [x 0.10] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, skinWidth));
|
||||
|
||||
// 4. CAPSULE CENTER
|
||||
// Center Y = (Height / 2) + SkinWidth
|
||||
float centerY = (finalHeight / 2f) + skinWidth;
|
||||
sb.AppendLine(string.Format("<b>4. CENTER Y:</b> ({0:F3}m / 2) + {1:F3}m (Skin) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, skinWidth, centerY));
|
||||
sb.AppendLine(string.Format("<b>5. CENTER Z:</b> [Fixed Offset] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
|
||||
|
||||
// 5. MOVEMENT CONSTRAINTS
|
||||
float stepOffset = finalHeight * 0.15f;
|
||||
sb.AppendLine(string.Format("<b>6. STEP OFFSET:</b> {0:F3}m (Height) [x 0.15] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, stepOffset));
|
||||
|
||||
// 6. GROUND CHECK (STATE MACHINE)
|
||||
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
||||
// if (stateMachine != null)
|
||||
// {
|
||||
// float groundCheckRadius = finalRadius * 0.6f;
|
||||
// float groundCheckOffsetY = finalHeight / 24f;
|
||||
// Vector3 groundCheckOffset = new Vector3(0, groundCheckOffsetY, zCenterOffset);
|
||||
//
|
||||
// stateMachine.SetGroundCheck(groundCheckRadius, groundCheckOffset);
|
||||
//
|
||||
// sb.AppendLine("<b>7. GROUND CHECK SETUP:</b>");
|
||||
// sb.AppendLine(string.Format(" - Radius: {0:F3}m (Radius) [x 0.60] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, groundCheckRadius));
|
||||
// sb.AppendLine(string.Format(" - Offset Y: {0:F3}m (Height) [/ 24] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, groundCheckOffsetY));
|
||||
// sb.AppendLine(string.Format(" - Offset Z: [Sync Center Z] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
|
||||
// }
|
||||
|
||||
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
||||
|
||||
// Apply to Controller
|
||||
controller.height = finalHeight;
|
||||
controller.radius = finalRadius;
|
||||
controller.skinWidth = skinWidth;
|
||||
controller.center = new Vector3(0, centerY, zCenterOffset);
|
||||
controller.slopeLimit = 45f;
|
||||
controller.stepOffset = stepOffset;
|
||||
controller.minMoveDistance = 0.001f;
|
||||
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
private Bounds GetRelativeBounds()
|
||||
{
|
||||
Transform targetRoot = modelRoot != null ? modelRoot : transform;
|
||||
Renderer[] renderers = targetRoot.GetComponentsInChildren<Renderer>();
|
||||
|
||||
if (renderers.Length == 0) return new Bounds(Vector3.zero, Vector3.zero);
|
||||
|
||||
// Using local bounds of SkinnedMeshRenderers for better accuracy on animated characters
|
||||
Bounds combinedLocalBounds = new Bounds();
|
||||
bool first = true;
|
||||
|
||||
foreach (Renderer renderer in renderers)
|
||||
{
|
||||
if (renderer is ParticleSystemRenderer) continue;
|
||||
|
||||
Bounds localB;
|
||||
if (renderer is SkinnedMeshRenderer smr)
|
||||
{
|
||||
localB = smr.localBounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For static meshes, convert world bounds back to local root space
|
||||
Vector3 min = transform.InverseTransformPoint(renderer.bounds.min);
|
||||
Vector3 max = transform.InverseTransformPoint(renderer.bounds.max);
|
||||
localB = new Bounds((min + max) / 2f, max - min);
|
||||
}
|
||||
|
||||
if (first)
|
||||
{
|
||||
combinedLocalBounds = localB;
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedLocalBounds.Encapsulate(localB);
|
||||
}
|
||||
}
|
||||
|
||||
return combinedLocalBounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e16a6690e589f0449ad89a6bf508ab62
|
||||
27
Assets/Scripts/Baba_yaga/GameSetup/CharacterSetupSettings.cs
Normal file
27
Assets/Scripts/Baba_yaga/GameSetup/CharacterSetupSettings.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup
|
||||
{
|
||||
[CreateAssetMenu(fileName = "CharacterSetupSettings", menuName = "BABA_YAGA/Setup/Character Setup Settings")]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts.GameSetup", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class CharacterSetupSettings : ScriptableObject
|
||||
{
|
||||
[Header("Movement Constraints")]
|
||||
public float slopeLimit = 45f;
|
||||
[Range(0.01f, 0.5f)] public float stepHeightRatio = 0.15f; // Step offset as % of height
|
||||
|
||||
[Header("Precision & Collision")]
|
||||
public float skinWidthRatio = 0.1f; // Skin width as % of radius
|
||||
public float minMoveDistance = 0.001f;
|
||||
|
||||
[Header("Dimension Multipliers")]
|
||||
[Tooltip("Multiplies the detected shoulder width to define Radius.")]
|
||||
public float radiusMultiplier = 0.8f;
|
||||
[Tooltip("Multiplies the detected bounding box height.")]
|
||||
public float heightMultiplier = 1.0f;
|
||||
|
||||
[Header("Center Offset")]
|
||||
[Tooltip("Y-axis offset for the center of the capsule (0.5 means exact middle).")]
|
||||
public float centerYRatio = 0.5f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d44cb4bd45c0e24bb3d8196a137db00
|
||||
42
Assets/Scripts/Baba_yaga/GameSetup/GameSettings.cs
Normal file
42
Assets/Scripts/Baba_yaga/GameSetup/GameSettings.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga
|
||||
{
|
||||
[CreateAssetMenu(fileName = "GameSettings", menuName = "BABA_YAGA/Settings/GameSettings")]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class GameSettings : ScriptableObject
|
||||
{
|
||||
[BoxGroup("Camera")]
|
||||
[PropertyRange(0.1f, 10f)]
|
||||
public float sensitivity = 1.0f;
|
||||
|
||||
[BoxGroup("Camera")]
|
||||
public bool invertX = false;
|
||||
|
||||
[BoxGroup("Camera")]
|
||||
public bool invertY = false;
|
||||
|
||||
[BoxGroup("Camera")]
|
||||
public bool sideBiasRight = true; // true for Right, false for Left
|
||||
|
||||
[BoxGroup("Camera")]
|
||||
[PropertyRange(40f, 110f)]
|
||||
public float fieldOfView = 60f;
|
||||
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
[BoxGroup("Camera")]
|
||||
private string SideBias => sideBiasRight ? "Right" : "Left";
|
||||
|
||||
[Button("Reset Defaults")]
|
||||
private void ResetDefaults()
|
||||
{
|
||||
sensitivity = 1.0f;
|
||||
invertX = false;
|
||||
invertY = false;
|
||||
sideBiasRight = true;
|
||||
fieldOfView = 60f;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/GameSetup/GameSettings.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/GameSetup/GameSettings.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e9fd2c44d7c5bc428b9b4eb12f4a7e1
|
||||
8
Assets/Scripts/Baba_yaga/GameSetup/Maze.meta
Normal file
8
Assets/Scripts/Baba_yaga/GameSetup/Maze.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6a10948eca4f3f4eaeda0611c778875
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
88
Assets/Scripts/Baba_yaga/GameSetup/Maze/CrawlerAlgorithm.cs
Normal file
88
Assets/Scripts/Baba_yaga/GameSetup/Maze/CrawlerAlgorithm.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class CrawlerAlgorithm : IMazeAlgorithm
|
||||
{
|
||||
private const int CrawlChance = 50;
|
||||
private const int MinBoundary = 1;
|
||||
private const int VerticalCrawlerCount = 3;
|
||||
private const int HorizontalCrawlerCount = 3;
|
||||
|
||||
public void Generate(MazeGrid grid)
|
||||
{
|
||||
for (int i = 0; i < VerticalCrawlerCount; i++) CrawlV(grid, 0);
|
||||
for (int i = 0; i < HorizontalCrawlerCount; i++) CrawlH(grid, 0);
|
||||
}
|
||||
|
||||
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
|
||||
{
|
||||
for (int i = 0; i < VerticalCrawlerCount; i++) yield return CrawlV(grid, cellsPerFrame);
|
||||
for (int i = 0; i < HorizontalCrawlerCount; i++) yield return CrawlH(grid, cellsPerFrame);
|
||||
}
|
||||
|
||||
private IEnumerator CrawlV(MazeGrid grid, int cellsPerFrame)
|
||||
{
|
||||
bool done = false;
|
||||
int x = Random.Range(MinBoundary, grid.Width - MinBoundary);
|
||||
int z = MinBoundary;
|
||||
|
||||
while (!done)
|
||||
{
|
||||
grid.SetCell(x, z, MazeCellType.Processing);
|
||||
MazeManager.cellsProcessedThisFrame++;
|
||||
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
|
||||
{
|
||||
MazeManager.cellsProcessedThisFrame = 0;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
|
||||
if (Random.Range(0, 100) < CrawlChance)
|
||||
{
|
||||
x += Random.Range(-1, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
z += Random.Range(0, 2);
|
||||
}
|
||||
|
||||
done |= (x < MinBoundary || x >= grid.Width - MinBoundary || z < MinBoundary || z >= grid.Depth - MinBoundary);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator CrawlH(MazeGrid grid, int cellsPerFrame)
|
||||
{
|
||||
bool done = false;
|
||||
int x = MinBoundary;
|
||||
int z = Random.Range(MinBoundary, grid.Depth - MinBoundary);
|
||||
|
||||
while (!done)
|
||||
{
|
||||
grid.SetCell(x, z, MazeCellType.Processing);
|
||||
MazeManager.cellsProcessedThisFrame++;
|
||||
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
|
||||
{
|
||||
MazeManager.cellsProcessedThisFrame = 0;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
|
||||
if (Random.Range(0, 100) < CrawlChance)
|
||||
{
|
||||
x += Random.Range(0, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
z += Random.Range(-1, 2);
|
||||
}
|
||||
|
||||
done |= (x < MinBoundary || x >= grid.Width - MinBoundary || z < MinBoundary || z >= grid.Depth - MinBoundary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd419f9be92beac48b6f551063165e1f
|
||||
25
Assets/Scripts/Baba_yaga/GameSetup/Maze/Extensions.cs
Normal file
25
Assets/Scripts/Baba_yaga/GameSetup/Maze/Extensions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze.Extensions
|
||||
{
|
||||
public static class ListExtensions
|
||||
{
|
||||
private static System.Random _rng = new System.Random();
|
||||
|
||||
/// <summary>
|
||||
/// Shuffles a list using the Fisher-Yates algorithm.
|
||||
/// </summary>
|
||||
public static void Shuffle<T>(this IList<T> list)
|
||||
{
|
||||
int n = list.Count;
|
||||
while (n > 1)
|
||||
{
|
||||
n--;
|
||||
int k = _rng.Next(n + 1);
|
||||
T value = list[k];
|
||||
list[k] = list[n];
|
||||
list[n] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d9791b1b03c14f16a245b2d4577c5f9
|
||||
8
Assets/Scripts/Baba_yaga/GameSetup/Maze/Interfaces.meta
Normal file
8
Assets/Scripts/Baba_yaga/GameSetup/Maze/Interfaces.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e1bb9cd9af7ffe40ad1a740c3c30dd6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for all maze generation algorithms.
|
||||
/// Supports both immediate and step-by-step (animated) generation.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public interface IMazeAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the maze immediately in one frame.
|
||||
/// </summary>
|
||||
void Generate(MazeGrid grid);
|
||||
|
||||
/// <summary>
|
||||
/// Generates the maze step-by-step for visualization.
|
||||
/// </summary>
|
||||
System.Collections.IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46b6a7796ba3c494581e4dcb884da064
|
||||
32
Assets/Scripts/Baba_yaga/GameSetup/Maze/MapLocation.cs
Normal file
32
Assets/Scripts/Baba_yaga/GameSetup/Maze/MapLocation.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a 2D coordinate on the maze grid.
|
||||
/// Used as a lightweight value type to avoid GC allocations.
|
||||
/// </summary>
|
||||
public readonly struct MapLocation
|
||||
{
|
||||
public readonly int x;
|
||||
public readonly int z;
|
||||
|
||||
public MapLocation(int _x, int _z)
|
||||
{
|
||||
x = _x;
|
||||
z = _z;
|
||||
}
|
||||
|
||||
// Static predefined directions to eliminate magic numbers in algorithms
|
||||
public static MapLocation Right => new MapLocation(1, 0);
|
||||
public static MapLocation Left => new MapLocation(-1, 0);
|
||||
public static MapLocation Up => new MapLocation(0, 1);
|
||||
public static MapLocation Down => new MapLocation(0, -1);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all 4 cardinal directions.
|
||||
/// </summary>
|
||||
public static System.Collections.Generic.List<MapLocation> Directions => new System.Collections.Generic.List<MapLocation>
|
||||
{
|
||||
Right, Up, Left, Down
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 987a7c46c96326a44b3a5f179fe61161
|
||||
43
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeCellType.cs
Normal file
43
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeCellType.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the state of each cell in the maze.
|
||||
/// Used to replace magic numbers and drive visual changes.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public enum MazeCellType
|
||||
{
|
||||
Wall, // Solid block
|
||||
Corridor, // Finalized path
|
||||
Processing, // Currently being evaluated by algorithm (Debug)
|
||||
Path, // Temporary path (e.g., Wilson's crawler)
|
||||
Start, // Entry point
|
||||
End, // Exit point
|
||||
StairsUp,
|
||||
StairsDown,
|
||||
Room
|
||||
}
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public enum PieceType
|
||||
{
|
||||
None,
|
||||
Wall,
|
||||
Vertical_Straight, // Đường thẳng dọc
|
||||
Horizontal_Straight, // Đường thẳng ngang
|
||||
Corner, // Góc cua
|
||||
T_Junction, // Ngã ba
|
||||
Crossroads, // Ngã tư
|
||||
Stairs,
|
||||
StairsUp// Cầu thang (Điểm nối)
|
||||
}
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public struct MazePieceData
|
||||
{
|
||||
public PieceType piece; // Hình dạng mảnh ghép
|
||||
// Bạn có thể thêm rotation nếu cần xoay hướng model sau này
|
||||
public int rotation;
|
||||
public GameObject model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f54ef08fa4922eb4a968d46c7aa71faf
|
||||
84
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeGrid.cs
Normal file
84
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeGrid.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the logical state of the maze grid.
|
||||
/// Notifies listeners whenever a cell changes to trigger visual updates.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class MazeGrid
|
||||
{
|
||||
public int Width { get; set; }
|
||||
public int Depth { get; set; }
|
||||
public int Level { get; set; }
|
||||
public MazePieceData[,] piecePlace;
|
||||
public float scale = 1f;
|
||||
|
||||
private readonly MazeCellType[,] _cells;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a cell's type is changed.
|
||||
/// Useful for the Renderer to trigger animations/FX.
|
||||
/// </summary>
|
||||
public event Action<int, int, MazeCellType> OnCellChanged;
|
||||
|
||||
public MazeGrid(int width, int depth)
|
||||
{
|
||||
Width = width;
|
||||
Depth = depth;
|
||||
piecePlace = new MazePieceData[width, depth];
|
||||
|
||||
_cells = new MazeCellType[width, depth];
|
||||
|
||||
// Initialize all as walls
|
||||
for (int z = 0; z < depth; z++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
_cells[x, z] = MazeCellType.Wall;
|
||||
piecePlace[x, z].piece = PieceType.Wall;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void SetCell(int x, int z, MazeCellType type)
|
||||
{
|
||||
if (IsInBounds(x, z))
|
||||
{
|
||||
if (_cells[x, z] != type)
|
||||
{
|
||||
_cells[x, z] = type;
|
||||
OnCellChanged?.Invoke(x, z, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MazeCellType GetCell(int x, int z)
|
||||
{
|
||||
if (IsInBounds(x, z))
|
||||
return _cells[x, z];
|
||||
return MazeCellType.Wall; // Treat out of bounds as walls
|
||||
}
|
||||
|
||||
public bool IsInBounds(int x, int z)
|
||||
{
|
||||
return x >= 0 && x < Width && z >= 0 && z < Depth;
|
||||
}
|
||||
|
||||
public int CountSquareNeighbours(int x, int z, MazeCellType targetType)
|
||||
{
|
||||
int count = 0;
|
||||
if (x <= 0 || x >= Width - 1 || z <= 0 || z >= Depth - 1) return 5;
|
||||
|
||||
if (GetCell(x - 1, z) == targetType) count++;
|
||||
if (GetCell(x + 1, z) == targetType) count++;
|
||||
if (GetCell(x, z + 1) == targetType) count++;
|
||||
if (GetCell(x, z - 1) == targetType) count++;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeGrid.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeGrid.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1a7a252ff0b1014a9690f08897e2e59
|
||||
361
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeManager.cs
Normal file
361
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeManager.cs
Normal file
@@ -0,0 +1,361 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
/// <summary>
|
||||
/// Central controller for the Maze system.
|
||||
/// Manages algorithm selection, debug speed, and regeneration.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class MazeManager : MonoBehaviour
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public enum AlgorithmType { Recursive, Wilsons, Prims, Crawler, NoiseRecursive }
|
||||
|
||||
[BoxGroup("Generation")]
|
||||
[InfoBox("Set the array size to control how many maze floors are generated. Runtime grid data is rebuilt when Regenerate runs.")]
|
||||
[ValidateInput(nameof(HasAtLeastOneFloor), "Maze Manager needs at least one floor.")]
|
||||
public MazeGrid[] mazes;
|
||||
|
||||
[BoxGroup("Generation")]
|
||||
[MinValue(0.001f)]
|
||||
public float floorHeight = 3.5f;
|
||||
|
||||
[BoxGroup("Generation")]
|
||||
[MinValue(0)]
|
||||
public int connectionsPerFloor = 2;
|
||||
|
||||
[BoxGroup("Generation")]
|
||||
[SerializeField] private AlgorithmType selectedAlgorithm;
|
||||
|
||||
[BoxGroup("Generation/Grid Size")]
|
||||
[PropertyRange(5, 200)]
|
||||
[SerializeField] private int width = 30;
|
||||
|
||||
[BoxGroup("Generation/Grid Size")]
|
||||
[PropertyRange(5, 200)]
|
||||
[SerializeField] private int depth = 30;
|
||||
|
||||
[BoxGroup("Animation")]
|
||||
[SerializeField] private bool animateGeneration = true;
|
||||
|
||||
[BoxGroup("Animation")]
|
||||
[ShowIf(nameof(animateGeneration))]
|
||||
[PropertyRange(1, 500)]
|
||||
[LabelText("Generation Speed (Cells/Frame)")]
|
||||
[SerializeField] private int generationSpeed = 50;
|
||||
|
||||
public static int cellsProcessedThisFrame;
|
||||
|
||||
[BoxGroup("Animation")]
|
||||
[ShowIf(nameof(animateGeneration))]
|
||||
public MazeRenderer.CellAnimationType cellAnimationType = MazeRenderer.CellAnimationType.ScaleUp;
|
||||
|
||||
[BoxGroup("Progress")]
|
||||
[ProgressBar(0, 100)]
|
||||
[ShowInInspector, ReadOnly]
|
||||
private float completionPercentage;
|
||||
|
||||
[BoxGroup("References")]
|
||||
[Required]
|
||||
[SerializeField] private MazeRenderer mazeRenderer;
|
||||
|
||||
[BoxGroup("Rooms (Phase 2)")]
|
||||
public bool generateRooms = true;
|
||||
[BoxGroup("Rooms (Phase 2)")]
|
||||
[ShowIf(nameof(generateRooms))]
|
||||
public int numberOfRooms = 2;
|
||||
[BoxGroup("Rooms (Phase 2)")]
|
||||
[ShowIf(nameof(generateRooms))]
|
||||
public Vector2Int minRoomSize = new Vector2Int(2, 2);
|
||||
[BoxGroup("Rooms (Phase 2)")]
|
||||
[ShowIf(nameof(generateRooms))]
|
||||
public Vector2Int maxRoomSize = new Vector2Int(4, 4);
|
||||
|
||||
[BoxGroup("References")]
|
||||
[Required]
|
||||
[SerializeField] private Transform mazeContainer;
|
||||
|
||||
[FoldoutGroup("Manhole Prefabs")]
|
||||
public GameObject straightManHoleLadder;
|
||||
[FoldoutGroup("Manhole Prefabs")]
|
||||
public GameObject straightManHoleUp;
|
||||
[FoldoutGroup("Manhole Prefabs")]
|
||||
public GameObject deadendManHoleLadder;
|
||||
[FoldoutGroup("Manhole Prefabs")]
|
||||
public GameObject deadendManHoleUp;
|
||||
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
[BoxGroup("Runtime")]
|
||||
private int FloorCount => mazes?.Length ?? 0;
|
||||
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
[BoxGroup("Runtime")]
|
||||
private string CurrentGrid => _grid == null ? "None" : $"{_grid.Width}x{_grid.Depth}, Level {_grid.Level}";
|
||||
|
||||
private MazeGrid _grid;
|
||||
private Coroutine _generationCoroutine;
|
||||
private HashSet<Vector3Int> _modifiedCells = new HashSet<Vector3Int>();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Regenerate();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.R))
|
||||
{
|
||||
Regenerate();
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Clear Maze")]
|
||||
[Button("Clear Maze", ButtonSizes.Large)]
|
||||
public void ClearMaze()
|
||||
{
|
||||
if (_generationCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_generationCoroutine);
|
||||
_generationCoroutine = null;
|
||||
}
|
||||
|
||||
if (mazeRenderer != null)
|
||||
{
|
||||
mazeRenderer.Clear();
|
||||
}
|
||||
|
||||
completionPercentage = 0f;
|
||||
_modifiedCells?.Clear();
|
||||
}
|
||||
|
||||
[ContextMenu("Regenerate")]
|
||||
[Button("Regenerate Maze", ButtonSizes.Large)]
|
||||
public void Regenerate()
|
||||
{
|
||||
if (mazeRenderer == null)
|
||||
{
|
||||
Debug.LogError("MazeManager needs a MazeRenderer reference before regenerating.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mazeContainer == null)
|
||||
{
|
||||
Debug.LogError("MazeManager needs a maze container reference before regenerating.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mazes == null || mazes.Length == 0)
|
||||
{
|
||||
mazes = new MazeGrid[1];
|
||||
}
|
||||
|
||||
ClearMaze();
|
||||
|
||||
mazeRenderer.currentAnimationType = cellAnimationType;
|
||||
|
||||
if (animateGeneration)
|
||||
{
|
||||
_generationCoroutine = StartCoroutine(GenerateMazeRoutine());
|
||||
}
|
||||
else
|
||||
{
|
||||
GenerateMazeInstant();
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateMazeInstant()
|
||||
{
|
||||
_modifiedCells.Clear();
|
||||
completionPercentage = 0f;
|
||||
|
||||
for (int i = 0; i < mazes.Length; i++)
|
||||
{
|
||||
mazes[i] = new MazeGrid(width, depth);
|
||||
mazes[i].Level = i;
|
||||
|
||||
CarveRooms(mazes[i]);
|
||||
|
||||
IMazeAlgorithm algorithmForFloor = GetAlgorithm(selectedAlgorithm);
|
||||
algorithmForFloor.Generate(mazes[i]);
|
||||
}
|
||||
|
||||
GenerateConnections();
|
||||
|
||||
for (int i = 0; i < mazes.Length; i++)
|
||||
{
|
||||
mazeRenderer.Initialize(mazes[i], mazeContainer, i == 0);
|
||||
}
|
||||
if (mazes.Length > 0) _grid = mazes[0];
|
||||
|
||||
completionPercentage = 100f;
|
||||
}
|
||||
|
||||
private IEnumerator GenerateMazeRoutine()
|
||||
{
|
||||
_modifiedCells.Clear();
|
||||
completionPercentage = 0f;
|
||||
|
||||
for (int i = 0; i < mazes.Length; i++)
|
||||
{
|
||||
mazes[i] = new MazeGrid(width, depth);
|
||||
mazes[i].Level = i;
|
||||
int floorIndex = i;
|
||||
|
||||
mazes[i].OnCellChanged += (x, z, type) =>
|
||||
{
|
||||
if (type != MazeCellType.Wall)
|
||||
{
|
||||
_modifiedCells.Add(new Vector3Int(x, floorIndex, z));
|
||||
int totalCells = width * depth * mazes.Length;
|
||||
// Approximate the progress to reach roughly 100% since algorithms don't visit all cells
|
||||
float fillRatio = 0.6f;
|
||||
completionPercentage = Mathf.Clamp((_modifiedCells.Count / ((float)totalCells * fillRatio)) * 100f, 0, 99f);
|
||||
}
|
||||
};
|
||||
|
||||
CarveRooms(mazes[i]);
|
||||
mazeRenderer.Initialize(mazes[i], mazeContainer, i == 0);
|
||||
|
||||
IMazeAlgorithm algorithmForFloor = GetAlgorithm(selectedAlgorithm);
|
||||
cellsProcessedThisFrame = 0;
|
||||
yield return StartCoroutine(algorithmForFloor.GenerateStepByStep(mazes[i], generationSpeed));
|
||||
}
|
||||
|
||||
GenerateConnections();
|
||||
|
||||
if (mazes.Length > 0) _grid = mazes[0];
|
||||
|
||||
completionPercentage = 100f;
|
||||
_generationCoroutine = null;
|
||||
}
|
||||
|
||||
private void GenerateConnections()
|
||||
{
|
||||
// Step 2: Create connections between adjacent floors
|
||||
for (int i = 0; i < mazes.Length - 1; i++)
|
||||
{
|
||||
MazeGrid currentFloor = mazes[i];
|
||||
MazeGrid nextFloor = mazes[i + 1];
|
||||
|
||||
List<Vector2Int> possibleConnections = new List<Vector2Int>();
|
||||
|
||||
for (int z = 0; z < depth; z++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
// Check if both floors have a corridor at this position
|
||||
bool isCurrentFloorPath = currentFloor.GetCell(x, z) == MazeCellType.Corridor;
|
||||
bool isNextFloorPath = nextFloor.GetCell(x, z) == MazeCellType.Corridor;
|
||||
|
||||
if (isCurrentFloorPath && isNextFloorPath)
|
||||
{
|
||||
possibleConnections.Add(new Vector2Int(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShuffleList(possibleConnections);
|
||||
|
||||
int connectionsMade = 0;
|
||||
foreach (Vector2Int pos in possibleConnections)
|
||||
{
|
||||
if (connectionsMade >= connectionsPerFloor) break;
|
||||
|
||||
int x = pos.x;
|
||||
int z = pos.y;
|
||||
|
||||
// Set stair cells
|
||||
currentFloor.SetCell(x, z, MazeCellType.StairsUp);
|
||||
nextFloor.SetCell(x, z, MazeCellType.StairsDown);
|
||||
|
||||
connectionsMade++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CarveRooms(MazeGrid grid)
|
||||
{
|
||||
if (!generateRooms) return;
|
||||
|
||||
for (int i = 0; i < numberOfRooms; i++)
|
||||
{
|
||||
int w = Random.Range(minRoomSize.x, maxRoomSize.x + 1);
|
||||
int d = Random.Range(minRoomSize.y, maxRoomSize.y + 1);
|
||||
|
||||
int startX = Random.Range(1, width - w - 1);
|
||||
int startZ = Random.Range(1, depth - d - 1);
|
||||
|
||||
for (int x = startX; x < startX + w; x++)
|
||||
{
|
||||
for (int z = startZ; z < startZ + d; z++)
|
||||
{
|
||||
grid.SetCell(x, z, MazeCellType.Room);
|
||||
}
|
||||
}
|
||||
|
||||
// Carve guaranteed door to seed pathfinding
|
||||
if (Random.value > 0.5f)
|
||||
{
|
||||
int doorX = Random.Range(startX, startX + w);
|
||||
int doorZ = Random.value > 0.5f ? startZ + d : startZ - 1;
|
||||
grid.SetCell(doorX, doorZ, MazeCellType.Corridor);
|
||||
}
|
||||
else
|
||||
{
|
||||
int doorX = Random.value > 0.5f ? startX + w : startX - 1;
|
||||
int doorZ = Random.Range(startZ, startZ + d);
|
||||
grid.SetCell(doorX, doorZ, MazeCellType.Corridor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShuffleList<T>(List<T> list)
|
||||
{
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
T temp = list[i];
|
||||
int randomIndex = Random.Range(i, list.Count);
|
||||
list[i] = list[randomIndex];
|
||||
list[randomIndex] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
private IMazeAlgorithm GetAlgorithm(AlgorithmType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
AlgorithmType.Recursive => new RecursiveAlgorithm(),
|
||||
AlgorithmType.Wilsons => new WilsonsAlgorithm(),
|
||||
AlgorithmType.Prims => new PrimsAlgorithm(),
|
||||
AlgorithmType.Crawler => new CrawlerAlgorithm(),
|
||||
AlgorithmType.NoiseRecursive => new NoiseRecursiveGenerator(),
|
||||
_ => new RecursiveAlgorithm()
|
||||
};
|
||||
}
|
||||
|
||||
[Button("Find Scene References")]
|
||||
private void FindSceneReferences()
|
||||
{
|
||||
if (mazeRenderer == null)
|
||||
{
|
||||
mazeRenderer = GetComponentInChildren<MazeRenderer>();
|
||||
}
|
||||
|
||||
if (mazeContainer == null)
|
||||
{
|
||||
mazeContainer = transform;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasAtLeastOneFloor(MazeGrid[] floors)
|
||||
{
|
||||
return floors != null && floors.Length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3607adabe0c29c34591af73b414eb17a
|
||||
489
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeRenderer.cs
Normal file
489
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeRenderer.cs
Normal file
@@ -0,0 +1,489 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
/// <summary>
|
||||
/// Responsible for the visual representation of the maze.
|
||||
/// Handles spawning, pooling, and animations with safety checks.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class MazeRenderer : MonoBehaviour
|
||||
{
|
||||
[BoxGroup("Visuals")]
|
||||
[Required]
|
||||
[InlineEditor]
|
||||
[SerializeField] private MazeVisualProfile visualProfile;
|
||||
|
||||
[BoxGroup("Visuals")]
|
||||
[MinValue(0.001f)]
|
||||
public float floorHeight = 3.5f;
|
||||
|
||||
public float Scale => visualProfile != null ? visualProfile.scale : 1f;
|
||||
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public enum CellAnimationType { None, ScaleUp, DropDown, SpinIn }
|
||||
|
||||
[HideInInspector]
|
||||
public CellAnimationType currentAnimationType = CellAnimationType.ScaleUp;
|
||||
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
[BoxGroup("Runtime")]
|
||||
private int SpawnedCellCount => _spawnedCells.Count;
|
||||
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
[BoxGroup("Runtime")]
|
||||
private int RenderedFloorCount => _grids.Count;
|
||||
|
||||
private readonly Dictionary<Vector3Int, GameObject> _spawnedCells = new Dictionary<Vector3Int, GameObject>();
|
||||
private Transform _container;
|
||||
private List<MazeGrid> _grids = new List<MazeGrid>();
|
||||
|
||||
public void Initialize(MazeGrid grid, Transform container, bool clearExisting = true)
|
||||
{
|
||||
if (visualProfile == null)
|
||||
{
|
||||
Debug.LogError("MazeRenderer needs a MazeVisualProfile before it can render.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (grid == null)
|
||||
{
|
||||
Debug.LogError("MazeRenderer received a null MazeGrid.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clearExisting)
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
_container = container;
|
||||
if (!_grids.Contains(grid))
|
||||
{
|
||||
_grids.Add(grid);
|
||||
grid.OnCellChanged += (x, z, type) => HandleCellChanged(grid, x, z, type);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
for (int z = 0; z < grid.Depth; z++)
|
||||
{
|
||||
for (int x = 0; x < grid.Width; x++)
|
||||
{
|
||||
UpdateCellVisual(grid, x, z, grid.GetCell(x, z), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
|
||||
foreach (var cell in _spawnedCells.Values)
|
||||
{
|
||||
if (cell == null) continue;
|
||||
|
||||
if (Application.isPlaying)
|
||||
Destroy(cell);
|
||||
else
|
||||
DestroyImmediate(cell);
|
||||
}
|
||||
|
||||
_spawnedCells.Clear();
|
||||
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
// Note: We can't easily unsubscribe because the lambda captures 'grid'.
|
||||
// In a production environment, we should use a proper event handler method.
|
||||
}
|
||||
_grids.Clear();
|
||||
}
|
||||
|
||||
[Button("Clear Spawned Maze")]
|
||||
private void ClearFromInspector()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
private void HandleCellChanged(MazeGrid grid, int x, int z, MazeCellType type)
|
||||
{
|
||||
UpdateCellVisual(grid, x, z, type, true);
|
||||
|
||||
UpdateNeighborVisual(grid, x + 1, z);
|
||||
UpdateNeighborVisual(grid, x - 1, z);
|
||||
UpdateNeighborVisual(grid, x, z + 1);
|
||||
UpdateNeighborVisual(grid, x, z - 1);
|
||||
}
|
||||
|
||||
private void UpdateNeighborVisual(MazeGrid grid, int x, int z)
|
||||
{
|
||||
if (grid != null && grid.IsInBounds(x, z))
|
||||
{
|
||||
if (IsPath(grid, x, z))
|
||||
{
|
||||
MazeCellType type = grid.GetCell(x, z);
|
||||
UpdateCellVisual(grid, x, z, type, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCellVisual(MazeGrid grid, int x, int z, MazeCellType type, bool animate)
|
||||
{
|
||||
Vector3Int posKey = new Vector3Int(x, grid.Level, z);
|
||||
|
||||
if (_spawnedCells.TryGetValue(posKey, out GameObject oldObj))
|
||||
{
|
||||
if (oldObj != null) DestroyImmediate(oldObj);
|
||||
_spawnedCells.Remove(posKey);
|
||||
}
|
||||
|
||||
if (type == MazeCellType.Wall) return;
|
||||
|
||||
float logicalSpacing = visualProfile.nodeSpacing; // Distances between Nodes
|
||||
float halfSpacing = logicalSpacing / 2f;
|
||||
float safeScale = Mathf.Max(0.001f, visualProfile.scale);
|
||||
float spacingScale = logicalSpacing * safeScale;
|
||||
|
||||
float yOffset = grid.Level * floorHeight;
|
||||
Vector3 localPos = new Vector3(x * spacingScale, yOffset, z * spacingScale);
|
||||
|
||||
GameObject cellParent = new GameObject($"Cell_{x}_{grid.Level}_{z}");
|
||||
cellParent.transform.SetParent(_container);
|
||||
cellParent.transform.localPosition = localPos;
|
||||
|
||||
bool spawnedAnything = false;
|
||||
|
||||
if (type == MazeCellType.Corridor || type == MazeCellType.Processing || type == MazeCellType.StairsUp || type == MazeCellType.StairsDown)
|
||||
{
|
||||
// 1. Spawn Node (Intersection, Corner, T, DeadEnd, or Stairs)
|
||||
GameObject nodePrefab;
|
||||
Quaternion nodeRot;
|
||||
Vector3 nodeOffset = Vector3.zero;
|
||||
|
||||
if (type == MazeCellType.StairsUp || type == MazeCellType.StairsDown)
|
||||
{
|
||||
nodePrefab = visualProfile.GetPrefab(type);
|
||||
nodeRot = Quaternion.Euler(0, visualProfile.stairsOffset, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
(nodePrefab, nodeRot, nodeOffset) = GetNodePrefabAndRotation(grid, x, z);
|
||||
}
|
||||
|
||||
if (nodePrefab != null)
|
||||
{
|
||||
GameObject node = Instantiate(nodePrefab, cellParent.transform);
|
||||
node.transform.localPosition = nodeOffset * safeScale;
|
||||
node.transform.localRotation = nodeRot;
|
||||
node.transform.localScale = Vector3.one * safeScale;
|
||||
spawnedAnything = true;
|
||||
}
|
||||
|
||||
// 2. Spawn Edge X (Right path)
|
||||
if (IsPath(grid, x + 1, z))
|
||||
{
|
||||
GameObject edgePrefab = visualProfile.corridorStraight;
|
||||
if (edgePrefab != null)
|
||||
{
|
||||
GameObject edgeX = Instantiate(edgePrefab, cellParent.transform);
|
||||
edgeX.transform.localPosition = new Vector3(halfSpacing * safeScale, 0, 0); // half units offset right
|
||||
edgeX.transform.localRotation = Quaternion.Euler(0, 90f, 0); // pointing along X
|
||||
edgeX.transform.localScale = Vector3.one * safeScale;
|
||||
spawnedAnything = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Spawn Edge Z (Top path)
|
||||
if (IsPath(grid, x, z + 1))
|
||||
{
|
||||
GameObject edgePrefab = visualProfile.corridorStraight;
|
||||
if (edgePrefab != null)
|
||||
{
|
||||
GameObject edgeZ = Instantiate(edgePrefab, cellParent.transform);
|
||||
edgeZ.transform.localPosition = new Vector3(0, 0, halfSpacing * safeScale); // half units offset forward
|
||||
edgeZ.transform.localRotation = Quaternion.identity; // pointing along Z
|
||||
edgeZ.transform.localScale = Vector3.one * safeScale;
|
||||
spawnedAnything = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == MazeCellType.Room)
|
||||
{
|
||||
// Spawn Floor
|
||||
if (visualProfile.roomFloorPrefab != null)
|
||||
{
|
||||
GameObject floor = Instantiate(visualProfile.roomFloorPrefab, cellParent.transform);
|
||||
floor.transform.localPosition = Vector3.zero;
|
||||
floor.transform.localScale = Vector3.one * safeScale;
|
||||
spawnedAnything = true;
|
||||
}
|
||||
|
||||
// Spawn Ceiling
|
||||
if (visualProfile.roomCeilingPrefab != null)
|
||||
{
|
||||
GameObject ceiling = Instantiate(visualProfile.roomCeilingPrefab, cellParent.transform);
|
||||
ceiling.transform.localPosition = new Vector3(0, floorHeight, 0);
|
||||
ceiling.transform.localScale = Vector3.one * safeScale;
|
||||
spawnedAnything = true;
|
||||
}
|
||||
|
||||
// Spawn Room Edges (Walls or Doors)
|
||||
MazeCellType top = grid.IsInBounds(x, z + 1) ? grid.GetCell(x, z + 1) : MazeCellType.Wall;
|
||||
SpawnRoomEdge(cellParent, top, new Vector3(0, 0, halfSpacing * safeScale), 0f, safeScale);
|
||||
|
||||
MazeCellType right = grid.IsInBounds(x + 1, z) ? grid.GetCell(x + 1, z) : MazeCellType.Wall;
|
||||
SpawnRoomEdge(cellParent, right, new Vector3(halfSpacing * safeScale, 0, 0), 90f, safeScale);
|
||||
|
||||
MazeCellType bottom = grid.IsInBounds(x, z - 1) ? grid.GetCell(x, z - 1) : MazeCellType.Wall;
|
||||
SpawnRoomEdge(cellParent, bottom, new Vector3(0, 0, -halfSpacing * safeScale), 180f, safeScale);
|
||||
|
||||
MazeCellType left = grid.IsInBounds(x - 1, z) ? grid.GetCell(x - 1, z) : MazeCellType.Wall;
|
||||
SpawnRoomEdge(cellParent, left, new Vector3(-halfSpacing * safeScale, 0, 0), 270f, safeScale);
|
||||
|
||||
spawnedAnything = true; // Always true if it reaches here
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-corridor logic (Start, End, etc)
|
||||
GameObject prefab = visualProfile.GetPrefab(type);
|
||||
if (prefab != null)
|
||||
{
|
||||
GameObject obj = Instantiate(prefab, cellParent.transform);
|
||||
obj.transform.localPosition = Vector3.zero;
|
||||
obj.transform.localRotation = Quaternion.identity;
|
||||
obj.transform.localScale = Vector3.one * safeScale;
|
||||
spawnedAnything = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!spawnedAnything)
|
||||
{
|
||||
DestroyImmediate(cellParent);
|
||||
return;
|
||||
}
|
||||
|
||||
_spawnedCells[posKey] = cellParent;
|
||||
|
||||
if (animate && visualProfile.animationDuration > 0)
|
||||
{
|
||||
StartCoroutine(AnimateCell(cellParent.transform));
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// THUẬT TOÁN BITMASK AUTO-TILING
|
||||
// =================================================================================
|
||||
private (GameObject, Quaternion, Vector3) GetNodePrefabAndRotation(MazeGrid grid, int x, int z)
|
||||
{
|
||||
bool top = IsPath(grid, x, z + 1);
|
||||
bool right = IsPath(grid, x + 1, z);
|
||||
bool bottom = IsPath(grid, x, z - 1);
|
||||
bool left = IsPath(grid, x - 1, z);
|
||||
|
||||
int mask = 0;
|
||||
if (top) mask += 1;
|
||||
if (right) mask += 2;
|
||||
if (bottom) mask += 4;
|
||||
if (left) mask += 8;
|
||||
|
||||
GameObject prefabToSpawn = null;
|
||||
float yRotation = 0f;
|
||||
Vector3 offset = Vector3.zero;
|
||||
|
||||
// Push dead ends to the boundary where edges end
|
||||
float endOffset = visualProfile.deadEndShift;
|
||||
|
||||
switch (mask)
|
||||
{
|
||||
case 1:
|
||||
prefabToSpawn = visualProfile.corridorDeadEnd;
|
||||
yRotation = 0f;
|
||||
offset = new Vector3(0, 0, endOffset);
|
||||
break;
|
||||
case 2:
|
||||
prefabToSpawn = visualProfile.corridorDeadEnd;
|
||||
yRotation = 90f;
|
||||
offset = new Vector3(endOffset, 0, 0);
|
||||
break;
|
||||
case 4:
|
||||
prefabToSpawn = visualProfile.corridorDeadEnd;
|
||||
yRotation = 180f;
|
||||
offset = new Vector3(0, 0, -endOffset);
|
||||
break;
|
||||
case 8:
|
||||
prefabToSpawn = visualProfile.corridorDeadEnd;
|
||||
yRotation = 270f;
|
||||
offset = new Vector3(-endOffset, 0, 0);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
prefabToSpawn = visualProfile.corridorStraight;
|
||||
yRotation = 0f;
|
||||
break;
|
||||
case 10:
|
||||
prefabToSpawn = visualProfile.corridorStraight;
|
||||
yRotation = 90f;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
prefabToSpawn = visualProfile.corridorCorner;
|
||||
yRotation = 0f;
|
||||
break;
|
||||
case 6:
|
||||
prefabToSpawn = visualProfile.corridorCorner;
|
||||
yRotation = 90f;
|
||||
break;
|
||||
case 12:
|
||||
prefabToSpawn = visualProfile.corridorCorner;
|
||||
yRotation = 180f;
|
||||
break;
|
||||
case 9:
|
||||
prefabToSpawn = visualProfile.corridorCorner;
|
||||
yRotation = 270f;
|
||||
break;
|
||||
|
||||
case 11:
|
||||
prefabToSpawn = visualProfile.corridorTJunction;
|
||||
yRotation = 0f;
|
||||
break;
|
||||
case 7:
|
||||
prefabToSpawn = visualProfile.corridorTJunction;
|
||||
yRotation = 90f;
|
||||
break;
|
||||
case 14:
|
||||
prefabToSpawn = visualProfile.corridorTJunction;
|
||||
yRotation = 180f;
|
||||
break;
|
||||
case 13:
|
||||
prefabToSpawn = visualProfile.corridorTJunction;
|
||||
yRotation = 270f;
|
||||
break;
|
||||
|
||||
case 15:
|
||||
prefabToSpawn = visualProfile.corridorCross;
|
||||
yRotation = 0f;
|
||||
break;
|
||||
|
||||
default:
|
||||
prefabToSpawn = visualProfile.corridorDeadEnd;
|
||||
yRotation = 0f;
|
||||
break;
|
||||
}
|
||||
|
||||
float finalRotation = yRotation;
|
||||
if (prefabToSpawn == visualProfile.corridorTJunction) finalRotation += visualProfile.tJunctionOffset;
|
||||
if (prefabToSpawn == visualProfile.corridorDeadEnd) finalRotation += visualProfile.deadEndOffset;
|
||||
if (prefabToSpawn == visualProfile.corridorCorner) finalRotation += visualProfile.cornerOffset;
|
||||
|
||||
if (prefabToSpawn == null) prefabToSpawn = visualProfile.corridorPrefab;
|
||||
|
||||
return (prefabToSpawn, Quaternion.Euler(0, finalRotation, 0), offset);
|
||||
}
|
||||
|
||||
private void SpawnRoomEdge(GameObject parent, MazeCellType neighborType, Vector3 offset, float yRot, float safeScale)
|
||||
{
|
||||
if (neighborType == MazeCellType.Room)
|
||||
return; // Open space to another room cell
|
||||
|
||||
GameObject prefabToSpawn = null;
|
||||
|
||||
if (neighborType == MazeCellType.Corridor || neighborType == MazeCellType.Processing || neighborType == MazeCellType.Start || neighborType == MazeCellType.End)
|
||||
{
|
||||
prefabToSpawn = visualProfile.roomDoorwayPrefab;
|
||||
}
|
||||
else
|
||||
{
|
||||
prefabToSpawn = visualProfile.roomWallPrefab;
|
||||
}
|
||||
|
||||
if (prefabToSpawn != null)
|
||||
{
|
||||
GameObject edge = Instantiate(prefabToSpawn, parent.transform);
|
||||
edge.transform.localPosition = offset;
|
||||
edge.transform.localRotation = Quaternion.Euler(0, yRot, 0);
|
||||
edge.transform.localScale = Vector3.one * safeScale;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPath(MazeGrid grid, int x, int z)
|
||||
{
|
||||
if (grid == null || !grid.IsInBounds(x, z)) return false;
|
||||
MazeCellType type = grid.GetCell(x, z);
|
||||
return type == MazeCellType.Corridor
|
||||
|| type == MazeCellType.Processing
|
||||
|| type == MazeCellType.Start
|
||||
|| type == MazeCellType.End
|
||||
|| type == MazeCellType.Path
|
||||
|| type == MazeCellType.StairsUp
|
||||
|| type == MazeCellType.StairsDown;
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// ANIMATION
|
||||
// =================================================================================
|
||||
private IEnumerator AnimateCell(Transform target)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
|
||||
if (currentAnimationType == CellAnimationType.None) yield break;
|
||||
|
||||
float duration = Mathf.Max(0.01f, visualProfile.animationDuration);
|
||||
float elapsed = 0;
|
||||
Vector3 finalScale = target.localScale;
|
||||
Vector3 finalPosition = target.localPosition;
|
||||
Quaternion finalRotation = target.localRotation;
|
||||
|
||||
if (currentAnimationType == CellAnimationType.ScaleUp)
|
||||
{
|
||||
target.localScale = Vector3.one * 0.001f;
|
||||
}
|
||||
else if (currentAnimationType == CellAnimationType.DropDown)
|
||||
{
|
||||
target.localPosition = finalPosition + Vector3.up * 5f;
|
||||
}
|
||||
else if (currentAnimationType == CellAnimationType.SpinIn)
|
||||
{
|
||||
target.localScale = Vector3.one * 0.001f;
|
||||
target.localRotation = finalRotation * Quaternion.Euler(0, 180, 0);
|
||||
}
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (target == null) yield break;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / duration);
|
||||
|
||||
if (currentAnimationType == CellAnimationType.ScaleUp)
|
||||
{
|
||||
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
|
||||
target.localScale = finalScale * Mathf.Max(0.001f, s);
|
||||
}
|
||||
else if (currentAnimationType == CellAnimationType.DropDown)
|
||||
{
|
||||
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
|
||||
target.localPosition = Vector3.Lerp(finalPosition + Vector3.up * 5f, finalPosition, s);
|
||||
}
|
||||
else if (currentAnimationType == CellAnimationType.SpinIn)
|
||||
{
|
||||
float s = Mathf.Sin(t * Mathf.PI * 0.5f);
|
||||
target.localScale = finalScale * Mathf.Max(0.001f, s);
|
||||
target.localRotation = Quaternion.Lerp(finalRotation * Quaternion.Euler(0, 180, 0), finalRotation, s);
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
target.localScale = finalScale;
|
||||
target.localPosition = finalPosition;
|
||||
target.localRotation = finalRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f30df611110713742ab984f5bead5d88
|
||||
147
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeVisualProfile.cs
Normal file
147
Assets/Scripts/Baba_yaga/GameSetup/Maze/MazeVisualProfile.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
[CreateAssetMenu(fileName = "MazeVisualProfile", menuName = "BABA_YAGA/Maze/Visual Profile")]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class MazeVisualProfile : ScriptableObject
|
||||
{
|
||||
[BoxGroup("Rotation Offsets")]
|
||||
public float tJunctionOffset = 0f;
|
||||
|
||||
[BoxGroup("Rotation Offsets")]
|
||||
public float cornerOffset = 0f;
|
||||
|
||||
[BoxGroup("Rotation Offsets")]
|
||||
public float deadEndOffset = 0f;
|
||||
|
||||
[BoxGroup("Rotation Offsets")]
|
||||
public float stairsOffset = 0f;
|
||||
|
||||
[BoxGroup("Cell Prefabs")]
|
||||
[Required]
|
||||
public GameObject wallPrefab;
|
||||
|
||||
[BoxGroup("Cell Prefabs")]
|
||||
[Required]
|
||||
public GameObject corridorPrefab;
|
||||
|
||||
[BoxGroup("Cell Prefabs")]
|
||||
public GameObject processingPrefab;
|
||||
|
||||
[BoxGroup("Cell Prefabs")]
|
||||
public GameObject pathPrefab;
|
||||
|
||||
[BoxGroup("Cell Prefabs")]
|
||||
public GameObject startPrefab;
|
||||
|
||||
[BoxGroup("Cell Prefabs")]
|
||||
public GameObject endPrefab;
|
||||
|
||||
[BoxGroup("Cell Prefabs")]
|
||||
[Required]
|
||||
public GameObject stairUpPrefab;
|
||||
|
||||
[BoxGroup("Cell Prefabs")]
|
||||
[Required]
|
||||
public GameObject stairDownPrefab;
|
||||
|
||||
[BoxGroup("Corridor Pieces")]
|
||||
[Required]
|
||||
public GameObject corridorStraight;
|
||||
|
||||
[BoxGroup("Corridor Pieces")]
|
||||
[Required]
|
||||
public GameObject corridorCorner;
|
||||
|
||||
[BoxGroup("Corridor Pieces")]
|
||||
[Required]
|
||||
public GameObject corridorTJunction;
|
||||
|
||||
[BoxGroup("Corridor Pieces")]
|
||||
[Required]
|
||||
public GameObject corridorCross;
|
||||
|
||||
[BoxGroup("Corridor Pieces")]
|
||||
[Required]
|
||||
public GameObject corridorDeadEnd;
|
||||
|
||||
[BoxGroup("Room Pieces (Phase 2)")]
|
||||
public GameObject roomFloorPrefab;
|
||||
[BoxGroup("Room Pieces (Phase 2)")]
|
||||
public GameObject roomWallPrefab;
|
||||
[BoxGroup("Room Pieces (Phase 2)")]
|
||||
public GameObject roomCeilingPrefab;
|
||||
[BoxGroup("Room Pieces (Phase 2)")]
|
||||
public GameObject roomDoorwayPrefab;
|
||||
|
||||
[BoxGroup("Visualization")]
|
||||
[MinValue(0.001f)]
|
||||
public float scale = 0.167f;
|
||||
|
||||
[BoxGroup("Visualization")]
|
||||
[MinValue(1f)]
|
||||
[InfoBox("The physical distance between each grid cell. Default is 6 based on 3x3 intersections and 3x2 halls.")]
|
||||
public float nodeSpacing = 6f;
|
||||
|
||||
[BoxGroup("Visualization")]
|
||||
[InfoBox("How far to push dead-end caps from the center so they touch the hallway edge. Default is 1.0.")]
|
||||
public float deadEndShift = 1.0f;
|
||||
|
||||
[BoxGroup("Visualization")]
|
||||
[MinValue(0f)]
|
||||
public float animationDuration = 0.25f;
|
||||
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
[BoxGroup("Validation")]
|
||||
private int MissingRequiredPrefabCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
if (wallPrefab == null) count++;
|
||||
if (corridorPrefab == null) count++;
|
||||
if (stairUpPrefab == null) count++;
|
||||
if (stairDownPrefab == null) count++;
|
||||
if (corridorStraight == null) count++;
|
||||
if (corridorCorner == null) count++;
|
||||
if (corridorTJunction == null) count++;
|
||||
if (corridorCross == null) count++;
|
||||
if (corridorDeadEnd == null) count++;
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject GetPrefab(MazeCellType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MazeCellType.Wall => wallPrefab,
|
||||
MazeCellType.Corridor => corridorPrefab,
|
||||
MazeCellType.Processing => processingPrefab,
|
||||
MazeCellType.Path => pathPrefab,
|
||||
MazeCellType.Start => startPrefab,
|
||||
MazeCellType.End => endPrefab,
|
||||
MazeCellType.StairsUp => stairUpPrefab,
|
||||
MazeCellType.StairsDown => stairDownPrefab,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
[Button("Log Missing Required Prefabs")]
|
||||
private void LogMissingRequiredPrefabs()
|
||||
{
|
||||
if (MissingRequiredPrefabCount == 0)
|
||||
{
|
||||
Debug.Log($"{name}: all required maze prefabs are assigned.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"{name}: {MissingRequiredPrefabCount} required maze prefab reference(s) are missing.", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3ff96571406a624381b7b0e596a4d1b
|
||||
8
Assets/Scripts/Baba_yaga/GameSetup/Maze/Native.meta
Normal file
8
Assets/Scripts/Baba_yaga/GameSetup/Maze/Native.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05fdc25279e7ac148a44fde646c93546
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze.Native
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze.Native", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class NativeNoiseProvider : IDisposable
|
||||
{
|
||||
private const string DLL_NAME = "BackroomsNoise";
|
||||
|
||||
[DllImport(DLL_NAME)]
|
||||
private static extern IntPtr CreateNoiseGenerator(int seed, float frequency, int noiseType);
|
||||
|
||||
[DllImport(DLL_NAME)]
|
||||
private static extern float GetNoiseValue(IntPtr handle, float x, float z);
|
||||
|
||||
[DllImport(DLL_NAME)]
|
||||
private static extern void GetNoiseBuffer(IntPtr handle, float startX, float startZ, int width, int depth, float[] buffer);
|
||||
|
||||
[DllImport(DLL_NAME)]
|
||||
private static extern void DestroyNoiseGenerator(IntPtr handle);
|
||||
|
||||
private IntPtr _handle;
|
||||
public bool IsInitialized => _handle != IntPtr.Zero;
|
||||
|
||||
public NativeNoiseProvider(int seed, float frequency = 0.01f, int noiseType = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
_handle = CreateNoiseGenerator(seed, frequency, noiseType);
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
Debug.LogWarning($"Native library '{DLL_NAME}' not found. Ensure it is compiled and placed in Plugins folder.");
|
||||
}
|
||||
}
|
||||
|
||||
public float GetNoise(float x, float z)
|
||||
{
|
||||
if (!IsInitialized) return 0f;
|
||||
return GetNoiseValue(_handle, x, z);
|
||||
}
|
||||
|
||||
public void FillBuffer(float startX, float startZ, int width, int depth, float[] buffer)
|
||||
{
|
||||
if (!IsInitialized || buffer == null) return;
|
||||
GetNoiseBuffer(_handle, startX, startZ, width, depth, buffer);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
DestroyNoiseGenerator(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
~NativeNoiseProvider()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4fd030227a1b87a4f8826f9b317fbf87
|
||||
@@ -0,0 +1,159 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Baba_yaga.GameSetup.Maze.Extensions;
|
||||
using Baba_yaga.GameSetup.Maze.Native;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
/// <summary>
|
||||
/// Advanced generator that combines C++ Native Noise with a Recursive Backtracking algorithm.
|
||||
/// Creates a hybrid layout of large rooms and chaotic corridors.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class NoiseRecursiveGenerator : IMazeAlgorithm
|
||||
{
|
||||
private readonly List<MapLocation> _directions = MapLocation.Directions;
|
||||
private float[] _noiseMap;
|
||||
private int _seed = 1337;
|
||||
|
||||
// Thresholds
|
||||
private const float RoomThreshold = 0.5f;
|
||||
private const float CorridorThreshold = -0.3f;
|
||||
private const int DeadEndNeighbourThreshold = 2;
|
||||
|
||||
public void Generate(MazeGrid grid)
|
||||
{
|
||||
InitializeNoise(grid);
|
||||
|
||||
// Step 1: Pre-place rooms based on noise peaks
|
||||
for (int z = 0; z < grid.Depth; z++)
|
||||
{
|
||||
for (int x = 0; x < grid.Width; x++)
|
||||
{
|
||||
float noise = GetNoiseAt(x, z, grid.Width);
|
||||
if (noise > RoomThreshold)
|
||||
{
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Run recursive carving in the "connectable" zones
|
||||
// We start from a few points to ensure coverage
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
int startX = Random.Range(1, grid.Width - 1);
|
||||
int startZ = Random.Range(1, grid.Depth - 1);
|
||||
GenerateRecursive(grid, startX, startZ);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
|
||||
{
|
||||
InitializeNoise(grid);
|
||||
|
||||
// Visual feedback for noise pre-placement
|
||||
for (int z = 0; z < grid.Depth; z++)
|
||||
{
|
||||
for (int x = 0; x < grid.Width; x++)
|
||||
{
|
||||
float noise = GetNoiseAt(x, z, grid.Width);
|
||||
if (noise > RoomThreshold)
|
||||
{
|
||||
grid.SetCell(x, z, MazeCellType.Processing);
|
||||
}
|
||||
}
|
||||
if (z % 5 == 0) yield return null;
|
||||
}
|
||||
|
||||
for (int z = 0; z < grid.Depth; z++)
|
||||
{
|
||||
for (int x = 0; x < grid.Width; x++)
|
||||
{
|
||||
if (grid.GetCell(x, z) == MazeCellType.Processing)
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
}
|
||||
}
|
||||
|
||||
yield return GenerateRecursiveStepByStep(grid, 5, 5, cellsPerFrame);
|
||||
}
|
||||
|
||||
private void InitializeNoise(MazeGrid grid)
|
||||
{
|
||||
_noiseMap = new float[grid.Width * grid.Depth];
|
||||
using (var provider = new NativeNoiseProvider(_seed, 0.05f))
|
||||
{
|
||||
if (provider.IsInitialized)
|
||||
{
|
||||
provider.FillBuffer(0, 0, grid.Width, grid.Depth, _noiseMap);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to Unity Perlin
|
||||
for (int z = 0; z < grid.Depth; z++)
|
||||
{
|
||||
for (int x = 0; x < grid.Width; x++)
|
||||
{
|
||||
_noiseMap[z * grid.Width + x] = Mathf.PerlinNoise(x * 0.1f, z * 0.1f) * 2f - 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float GetNoiseAt(int x, int z, int width)
|
||||
{
|
||||
if (_noiseMap == null) return 0f;
|
||||
return _noiseMap[z * width + x];
|
||||
}
|
||||
|
||||
private void GenerateRecursive(MazeGrid grid, int x, int z)
|
||||
{
|
||||
if (!grid.IsInBounds(x, z)) return;
|
||||
if (grid.GetCell(x, z) != MazeCellType.Wall) return;
|
||||
if (GetNoiseAt(x, z, grid.Width) < CorridorThreshold) return;
|
||||
if (grid.GetCell(x, z) == MazeCellType.Corridor) return;
|
||||
|
||||
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) return;
|
||||
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
|
||||
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
|
||||
shuffledDirs.Shuffle();
|
||||
|
||||
foreach (var dir in shuffledDirs)
|
||||
{
|
||||
GenerateRecursive(grid, x + dir.x, z + dir.z);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator GenerateRecursiveStepByStep(MazeGrid grid, int x, int z, int cellsPerFrame)
|
||||
{
|
||||
if (!grid.IsInBounds(x, z)) yield break;
|
||||
if (grid.GetCell(x, z) != MazeCellType.Wall) yield break;
|
||||
if (GetNoiseAt(x, z, grid.Width) < CorridorThreshold) yield break;
|
||||
if (grid.GetCell(x, z) == MazeCellType.Corridor) yield break;
|
||||
|
||||
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) yield break;
|
||||
|
||||
grid.SetCell(x, z, MazeCellType.Processing);
|
||||
MazeManager.cellsProcessedThisFrame++;
|
||||
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
|
||||
{
|
||||
MazeManager.cellsProcessedThisFrame = 0;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
|
||||
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
|
||||
shuffledDirs.Shuffle();
|
||||
|
||||
foreach (var dir in shuffledDirs)
|
||||
{
|
||||
yield return GenerateRecursiveStepByStep(grid, x + dir.x, z + dir.z, cellsPerFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22e17049420e98f43a692fd3e7d7d261
|
||||
112
Assets/Scripts/Baba_yaga/GameSetup/Maze/PrimsAlgorithm.cs
Normal file
112
Assets/Scripts/Baba_yaga/GameSetup/Maze/PrimsAlgorithm.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class PrimsAlgorithm : IMazeAlgorithm
|
||||
{
|
||||
private const int InitialX = 2;
|
||||
private const int InitialZ = 2;
|
||||
private const int MaxIterations = 10000;
|
||||
private const int TargetCorridorNeighbours = 1;
|
||||
|
||||
public void Generate(MazeGrid grid)
|
||||
{
|
||||
int x = InitialX;
|
||||
int z = InitialZ;
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
|
||||
List<MapLocation> walls = GetNeighbouringWalls(grid, x, z);
|
||||
|
||||
int iterations = 0;
|
||||
while (walls.Count > 0 && iterations < MaxIterations)
|
||||
{
|
||||
int rIndex = Random.Range(0, walls.Count);
|
||||
MapLocation w = walls[rIndex];
|
||||
walls.RemoveAt(rIndex);
|
||||
|
||||
if (grid.CountSquareNeighbours(w.x, w.z, MazeCellType.Corridor) == TargetCorridorNeighbours)
|
||||
{
|
||||
grid.SetCell(w.x, w.z, MazeCellType.Corridor);
|
||||
|
||||
foreach (var nw in GetNeighbouringWalls(grid, w.x, w.z))
|
||||
{
|
||||
if (!walls.Contains(nw)) walls.Add(nw);
|
||||
}
|
||||
}
|
||||
iterations++;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
|
||||
{
|
||||
int x = InitialX;
|
||||
int z = InitialZ;
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
yield return null;
|
||||
|
||||
List<MapLocation> walls = GetNeighbouringWalls(grid, x, z);
|
||||
foreach(var w in walls) grid.SetCell(w.x, w.z, MazeCellType.Processing);
|
||||
|
||||
int iterations = 0;
|
||||
while (walls.Count > 0 && iterations < MaxIterations)
|
||||
{
|
||||
int rIndex = Random.Range(0, walls.Count);
|
||||
MapLocation w = walls[rIndex];
|
||||
walls.RemoveAt(rIndex);
|
||||
|
||||
if (grid.CountSquareNeighbours(w.x, w.z, MazeCellType.Corridor) == TargetCorridorNeighbours)
|
||||
{
|
||||
grid.SetCell(w.x, w.z, MazeCellType.Corridor);
|
||||
MazeManager.cellsProcessedThisFrame++;
|
||||
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
|
||||
{
|
||||
MazeManager.cellsProcessedThisFrame = 0;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
foreach (var nw in GetNeighbouringWalls(grid, w.x, w.z))
|
||||
{
|
||||
if (grid.GetCell(nw.x, nw.z) == MazeCellType.Wall)
|
||||
{
|
||||
grid.SetCell(nw.x, nw.z, MazeCellType.Processing);
|
||||
walls.Add(nw);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it's no longer a candidate, turn it back to Wall
|
||||
grid.SetCell(w.x, w.z, MazeCellType.Wall);
|
||||
}
|
||||
iterations++;
|
||||
}
|
||||
}
|
||||
|
||||
private List<MapLocation> GetNeighbouringWalls(MazeGrid grid, int x, int z)
|
||||
{
|
||||
List<MapLocation> neighbours = new List<MapLocation>();
|
||||
foreach (var dir in MapLocation.Directions)
|
||||
{
|
||||
int nx = x + dir.x;
|
||||
int nz = z + dir.z;
|
||||
|
||||
// Correction
|
||||
nx = x + dir.x;
|
||||
nz = z + dir.z;
|
||||
|
||||
if (grid.IsInBounds(nx, nz))
|
||||
{
|
||||
MazeCellType type = grid.GetCell(nx, nz);
|
||||
if (type == MazeCellType.Wall || type == MazeCellType.Processing)
|
||||
{
|
||||
neighbours.Add(new MapLocation(nx, nz));
|
||||
}
|
||||
}
|
||||
}
|
||||
return neighbours;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edcdd3c0aa9656a4797b83cc675aa629
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Baba_yaga.GameSetup.Maze.Extensions;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class RecursiveAlgorithm : IMazeAlgorithm
|
||||
{
|
||||
private const int StartX = 5;
|
||||
private const int StartZ = 5;
|
||||
private const int DeadEndNeighbourThreshold = 2;
|
||||
|
||||
private readonly List<MapLocation> _directions = MapLocation.Directions;
|
||||
|
||||
public void Generate(MazeGrid grid)
|
||||
{
|
||||
GenerateRecursive(grid, StartX, StartZ);
|
||||
}
|
||||
|
||||
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
|
||||
{
|
||||
yield return GenerateRecursiveStepByStep(grid, StartX, StartZ, cellsPerFrame);
|
||||
}
|
||||
|
||||
private void GenerateRecursive(MazeGrid grid, int x, int z)
|
||||
{
|
||||
if (grid.GetCell(x, z) != MazeCellType.Wall) return;
|
||||
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) return;
|
||||
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
|
||||
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
|
||||
shuffledDirs.Shuffle();
|
||||
|
||||
foreach (var dir in shuffledDirs)
|
||||
{
|
||||
int nx = x + dir.x;
|
||||
int nz = z + dir.z;
|
||||
if (grid.IsInBounds(nx, nz))
|
||||
{
|
||||
GenerateRecursive(grid, nx, nz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator GenerateRecursiveStepByStep(MazeGrid grid, int x, int z, int cellsPerFrame)
|
||||
{
|
||||
if (grid.GetCell(x, z) != MazeCellType.Wall) yield break;
|
||||
if (grid.CountSquareNeighbours(x, z, MazeCellType.Corridor) >= DeadEndNeighbourThreshold) yield break;
|
||||
|
||||
grid.SetCell(x, z, MazeCellType.Processing);
|
||||
MazeManager.cellsProcessedThisFrame++;
|
||||
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
|
||||
{
|
||||
MazeManager.cellsProcessedThisFrame = 0;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
|
||||
List<MapLocation> shuffledDirs = new List<MapLocation>(_directions);
|
||||
shuffledDirs.Shuffle();
|
||||
|
||||
foreach (var dir in shuffledDirs)
|
||||
{
|
||||
int nx = x + dir.x;
|
||||
int nz = z + dir.z;
|
||||
if (grid.IsInBounds(nx, nz))
|
||||
{
|
||||
yield return GenerateRecursiveStepByStep(grid, nx, nz, cellsPerFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2460c0e9379da9741b3d3387aa6c7a8e
|
||||
182
Assets/Scripts/Baba_yaga/GameSetup/Maze/WilsonsAlgorithm.cs
Normal file
182
Assets/Scripts/Baba_yaga/GameSetup/Maze/WilsonsAlgorithm.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga.GameSetup.Maze
|
||||
{
|
||||
/// <summary>
|
||||
/// Wilson's Algorithm implementation based on the original provided logic.
|
||||
/// Ensures paths are sparse and correctly finalized using specific neighbor constraints.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.GameSetup.Maze", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class WilsonsAlgorithm : IMazeAlgorithm
|
||||
{
|
||||
private const int MinBoundary = 2;
|
||||
private const int MaxIterationSafety = 5000;
|
||||
private const int MaxWalkSteps = 5000;
|
||||
|
||||
private readonly List<MapLocation> _directions = MapLocation.Directions;
|
||||
private List<MapLocation> _notUsed = new List<MapLocation>();
|
||||
|
||||
public void Generate(MazeGrid grid)
|
||||
{
|
||||
// 1. Create a starting finalized cell (Type.Corridor represents state 2)
|
||||
int x = Random.Range(MinBoundary, grid.Width - 1);
|
||||
int z = Random.Range(MinBoundary, grid.Depth - 1);
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
|
||||
int safety = 0;
|
||||
while (GetAvailableCells(grid) > 1 && safety < MaxIterationSafety)
|
||||
{
|
||||
RandomWalkSync(grid);
|
||||
safety++;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator GenerateStepByStep(MazeGrid grid, int cellsPerFrame)
|
||||
{
|
||||
int x = Random.Range(MinBoundary, grid.Width - 1);
|
||||
int z = Random.Range(MinBoundary, grid.Depth - 1);
|
||||
grid.SetCell(x, z, MazeCellType.Corridor);
|
||||
yield return null;
|
||||
|
||||
int safety = 0;
|
||||
while (GetAvailableCells(grid) > 1 && safety < MaxIterationSafety)
|
||||
{
|
||||
yield return RandomWalk(grid, cellsPerFrame);
|
||||
safety++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts neighbors that are already part of the finalized maze (State 2 / Corridor).
|
||||
/// </summary>
|
||||
private int CountFinalizedNeighbours(MazeGrid grid, int x, int z)
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var d in _directions)
|
||||
{
|
||||
if (grid.GetCell(x + d.x, z + d.z) == MazeCellType.Corridor)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private int GetAvailableCells(MazeGrid grid)
|
||||
{
|
||||
_notUsed.Clear();
|
||||
for (int z = 1; z < grid.Depth - 1; z++)
|
||||
{
|
||||
for (int x = 1; x < grid.Width - 1; x++)
|
||||
{
|
||||
if (CountFinalizedNeighbours(grid, x, z) == 0)
|
||||
{
|
||||
_notUsed.Add(new MapLocation(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
return _notUsed.Count;
|
||||
}
|
||||
|
||||
private void RandomWalkSync(MazeGrid grid)
|
||||
{
|
||||
if (_notUsed.Count == 0) return;
|
||||
|
||||
List<MapLocation> inWalk = new List<MapLocation>();
|
||||
int rStartIndex = Random.Range(0, _notUsed.Count);
|
||||
int cx = _notUsed[rStartIndex].x;
|
||||
int cz = _notUsed[rStartIndex].z;
|
||||
|
||||
inWalk.Add(new MapLocation(cx, cz));
|
||||
|
||||
int loop = 0;
|
||||
bool validPath = false;
|
||||
while (cx > 0 && cx < grid.Width - 1 && cz > 0 && cz < grid.Depth - 1 && loop < MaxWalkSteps && !validPath)
|
||||
{
|
||||
// Mark as temporary walk (State 0 / Processing)
|
||||
// Note: We don't set grid cell here in sync mode to avoid triggering events unnecessarily
|
||||
// but we keep track of neighbors.
|
||||
|
||||
if (CountFinalizedNeighbours(grid, cx, cz) > 1) break;
|
||||
|
||||
MapLocation rd = _directions[Random.Range(0, _directions.Count)];
|
||||
int nx = cx + rd.x;
|
||||
int nz = cz + rd.z;
|
||||
|
||||
// User's original constraint: CountSquareNeighbours (nx, nz) < 2
|
||||
if (CountFinalizedNeighbours(grid, nx, nz) < 2)
|
||||
{
|
||||
cx = nx;
|
||||
cz = nz;
|
||||
inWalk.Add(new MapLocation(cx, cz));
|
||||
}
|
||||
|
||||
validPath = CountFinalizedNeighbours(grid, cx, cz) == 1;
|
||||
loop++;
|
||||
}
|
||||
|
||||
if (validPath)
|
||||
{
|
||||
foreach (MapLocation m in inWalk)
|
||||
grid.SetCell(m.x, m.z, MazeCellType.Corridor);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator RandomWalk(MazeGrid grid, int cellsPerFrame)
|
||||
{
|
||||
if (_notUsed.Count == 0) yield break;
|
||||
|
||||
List<MapLocation> inWalk = new List<MapLocation>();
|
||||
int rStartIndex = Random.Range(0, _notUsed.Count);
|
||||
int cx = _notUsed[rStartIndex].x;
|
||||
int cz = _notUsed[rStartIndex].z;
|
||||
|
||||
inWalk.Add(new MapLocation(cx, cz));
|
||||
|
||||
int loop = 0;
|
||||
bool validPath = false;
|
||||
while (cx > 0 && cx < grid.Width - 1 && cz > 0 && cz < grid.Depth - 1 && loop < MaxWalkSteps && !validPath)
|
||||
{
|
||||
grid.SetCell(cx, cz, MazeCellType.Processing); // State 0
|
||||
MazeManager.cellsProcessedThisFrame++;
|
||||
if (MazeManager.cellsProcessedThisFrame >= cellsPerFrame)
|
||||
{
|
||||
MazeManager.cellsProcessedThisFrame = 0;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (CountFinalizedNeighbours(grid, cx, cz) > 1) break;
|
||||
|
||||
MapLocation rd = _directions[Random.Range(0, _directions.Count)];
|
||||
int nx = cx + rd.x;
|
||||
int nz = cz + rd.z;
|
||||
|
||||
if (CountFinalizedNeighbours(grid, nx, nz) < 2)
|
||||
{
|
||||
cx = nx;
|
||||
cz = nz;
|
||||
inWalk.Add(new MapLocation(cx, cz));
|
||||
}
|
||||
|
||||
validPath = CountFinalizedNeighbours(grid, cx, cz) == 1;
|
||||
loop++;
|
||||
}
|
||||
|
||||
if (validPath)
|
||||
{
|
||||
foreach (MapLocation m in inWalk)
|
||||
{
|
||||
grid.SetCell(m.x, m.z, MazeCellType.Corridor); // State 2
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (MapLocation m in inWalk)
|
||||
grid.SetCell(m.x, m.z, MazeCellType.Wall); // State 1
|
||||
}
|
||||
inWalk.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61156f8986612ca49a0672ad5542380f
|
||||
15
Assets/Scripts/Baba_yaga/GameSetup/ObjectInteraction.cs
Normal file
15
Assets/Scripts/Baba_yaga/GameSetup/ObjectInteraction.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(fileName = "ObjectInteraction", menuName = "BABA_YAGA/Scriptable Objects/ObjectInteraction")]
|
||||
public class ObjectInteraction : ScriptableObject
|
||||
{
|
||||
[Header("UI Settings")]
|
||||
public string promptText = "Interact";
|
||||
|
||||
[Header("Audio & Visuals")]
|
||||
public AudioClip interactionSound;
|
||||
public GameObject interactionVFX;
|
||||
|
||||
[Header("Settings")]
|
||||
public float interactionCooldown = 0.5f;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7edbad1c5ab86741af25e92524a1350
|
||||
79
Assets/Scripts/Baba_yaga/GameSetup/SettingsManager.cs
Normal file
79
Assets/Scripts/Baba_yaga/GameSetup/SettingsManager.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class SettingsManager : MonoBehaviour
|
||||
{
|
||||
public static SettingsManager Instance { get; private set; }
|
||||
|
||||
[BoxGroup("Settings")]
|
||||
[Required]
|
||||
[InlineEditor]
|
||||
[SerializeField] private GameSettings settings;
|
||||
public GameSettings Settings => settings;
|
||||
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
[BoxGroup("Runtime")]
|
||||
private bool IsActiveInstance => Instance == this;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
// Fallback or load from Resources if needed
|
||||
settings = ScriptableObject.CreateInstance<GameSettings>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSensitivity(float value)
|
||||
{
|
||||
settings.sensitivity = value;
|
||||
OnSettingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SetInvertX(bool value)
|
||||
{
|
||||
settings.invertX = value;
|
||||
OnSettingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SetInvertY(bool value)
|
||||
{
|
||||
settings.invertY = value;
|
||||
OnSettingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SetSideBias(bool isRight)
|
||||
{
|
||||
settings.sideBiasRight = isRight;
|
||||
OnSettingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void ToggleSideBias()
|
||||
{
|
||||
settings.sideBiasRight = !settings.sideBiasRight;
|
||||
OnSettingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
[Button("Notify Settings Changed")]
|
||||
private void NotifySettingsChanged()
|
||||
{
|
||||
OnSettingsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public event System.Action OnSettingsChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86e70fc045fbf71469903c69f7f54e67
|
||||
8
Assets/Scripts/Baba_yaga/Interaction.meta
Normal file
8
Assets/Scripts/Baba_yaga/Interaction.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f465518e0a6dc0e40982d7ffdfda36ac
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Baba_yaga/Manager.meta
Normal file
8
Assets/Scripts/Baba_yaga/Manager.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a48b89f983fff0642987cca2cb779dd4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
37
Assets/Scripts/Baba_yaga/Manager/GameManager.cs
Normal file
37
Assets/Scripts/Baba_yaga/Manager/GameManager.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Fusion;
|
||||
using TMPro;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class GameManager : NetworkBehaviour
|
||||
{
|
||||
public Text gameOverText; // Reference to the Game Over text UI element
|
||||
|
||||
private bool isGameOver = false; // Flag to check if the game is over
|
||||
|
||||
private void Start() {
|
||||
if (gameOverText != null) {
|
||||
// Ensure the Game Over text is hidden at the start of the game
|
||||
gameOverText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
[SerializeField] private Baba_yaga.Network.MatchEloManager eloManager;
|
||||
|
||||
public void TriggerGameOver(PlayerRef winner, PlayerRef loser, bool isDraw = false) {
|
||||
if (!isGameOver) {
|
||||
isGameOver = true;
|
||||
|
||||
if (gameOverText != null) {
|
||||
gameOverText.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
// Only Host processes Elo
|
||||
if (Runner.IsServer && eloManager != null) {
|
||||
eloManager.ProcessMatchResult(winner, loser, isDraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Manager/GameManager.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Manager/GameManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1180f4b733c7599498f6eb4e77848e23
|
||||
8
Assets/Scripts/Baba_yaga/Network.meta
Normal file
8
Assets/Scripts/Baba_yaga/Network.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f96029db35b52ba4182888a7f14d2ea7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
498
Assets/Scripts/Baba_yaga/Network/BasicSpawner.cs
Normal file
498
Assets/Scripts/Baba_yaga/Network/BasicSpawner.cs
Normal file
@@ -0,0 +1,498 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Fusion;
|
||||
using Fusion.Sockets;
|
||||
using UnityEngine;
|
||||
using Baba_yaga;
|
||||
|
||||
namespace Baba_yaga.UI
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.UI", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks
|
||||
{
|
||||
private static BasicSpawner _instance;
|
||||
public static BasicSpawner Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = UnityEngine.Object.FindFirstObjectByType<BasicSpawner>();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private NetworkRunner _runner;
|
||||
public NetworkRunner Runner => _runner;
|
||||
|
||||
private bool _isStarting = false;
|
||||
private bool _isInternalShutdown = false;
|
||||
|
||||
public event Action<List<SessionInfo>> OnSessionListUpdatedEvent;
|
||||
public event Action<string> OnShutdownEvent;
|
||||
public event Action OnJoinStartedEvent;
|
||||
public event Action OnJoinFailedEvent;
|
||||
|
||||
[Header("Prefabs")]
|
||||
[SerializeField] private NetworkPrefabRef _playerPrefab;
|
||||
[SerializeField] private NetworkPrefabRef _playerDataManagerPrefab;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
_instance = this;
|
||||
|
||||
// Ensure this is a root object so DontDestroyOnLoad works correctly
|
||||
transform.SetParent(null);
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private async void Start()
|
||||
{
|
||||
// Auto-connect if we bypass the UI and start directly in the Main Scene
|
||||
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "Main Scene")
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Auto-starting Fusion in AutoHostOrClient mode for testing...");
|
||||
|
||||
if (_isStarting) return;
|
||||
_isStarting = true;
|
||||
|
||||
try
|
||||
{
|
||||
await EnsureRunnerExists();
|
||||
var sceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>();
|
||||
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
|
||||
|
||||
var result = await _runner.StartGame(new StartGameArgs()
|
||||
{
|
||||
GameMode = GameMode.Shared,
|
||||
SessionName = "QuickTestRoom", // Hardcoded session for instant testing
|
||||
SceneManager = sceneManager,
|
||||
Scene = SceneRef.FromIndex(UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex)
|
||||
});
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Auto Connect SUCCESS!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[BasicSpawner] Auto Connect FAILED: {result.ShutdownReason}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isStarting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerProfile LocalPlayerProfile { get; private set; }
|
||||
public void SetLocalPlayerProfile(PlayerProfile _profile)
|
||||
{
|
||||
LocalPlayerProfile = _profile;
|
||||
}
|
||||
|
||||
private async Task EnsureRunnerExists()
|
||||
{
|
||||
if (_runner != null)
|
||||
{
|
||||
_isInternalShutdown = true;
|
||||
try
|
||||
{
|
||||
if (_runner.IsRunning)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Shutting down existing runner before recreation.");
|
||||
await _runner.Shutdown();
|
||||
}
|
||||
|
||||
// Check if it still exists (Unity pseudo-null check)
|
||||
if (_runner != null)
|
||||
{
|
||||
// Only log if it's actually a valid object to destroy
|
||||
// If it's already marked for destruction, Unity == null will be true soon
|
||||
Destroy(_runner);
|
||||
}
|
||||
_runner = null;
|
||||
|
||||
await Task.Yield();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isInternalShutdown = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this == null) return; // BasicSpawner itself might be destroyed
|
||||
|
||||
_runner = gameObject.GetComponent<NetworkRunner>();
|
||||
if (_runner == null)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Creating new NetworkRunner component.");
|
||||
_runner = gameObject.AddComponent<NetworkRunner>();
|
||||
}
|
||||
|
||||
_runner.ProvideInput = true;
|
||||
_runner.AddCallbacks(this);
|
||||
}
|
||||
|
||||
public async Task StartLobby()
|
||||
{
|
||||
if (_isStarting) return;
|
||||
|
||||
// Nếu đã ở trong lobby rồi thì không cần làm gì
|
||||
if (_runner != null && _runner.IsRunning && _runner.LobbyInfo.IsValid) return;
|
||||
|
||||
Debug.Log("[BasicSpawner] StartLobby called");
|
||||
_isStarting = true;
|
||||
|
||||
try
|
||||
{
|
||||
await EnsureRunnerExists();
|
||||
Debug.Log("[BasicSpawner] Joining Lobby...");
|
||||
var result = await _runner.JoinSessionLobby(SessionLobby.ClientServer);
|
||||
if (!result.Ok)
|
||||
{
|
||||
Debug.LogWarning($"Join lobby result: {result.ShutdownReason}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StartHost(string sessionName, string displayName, string password = null)
|
||||
{
|
||||
// Wait for any existing startup process (like StartLobby) to finish
|
||||
while (_isStarting)
|
||||
{
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
_isStarting = true;
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log($"[BasicSpawner] StartHost called: {sessionName} ({displayName})");
|
||||
OnJoinStartedEvent?.Invoke();
|
||||
|
||||
bool sceneExists = false;
|
||||
for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCountInBuildSettings; i++)
|
||||
{
|
||||
if (UnityEngine.SceneManagement.SceneUtility.GetScenePathByBuildIndex(i).Contains("Main Scene"))
|
||||
{
|
||||
sceneExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sceneExists)
|
||||
{
|
||||
Debug.LogError("CRITICAL: 'Main Scene' is NOT in Build Settings!");
|
||||
return false;
|
||||
}
|
||||
|
||||
await EnsureRunnerExists();
|
||||
|
||||
var customProps = new Dictionary<string, SessionProperty>();
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
customProps.Add("pw", password);
|
||||
}
|
||||
customProps.Add("rn", displayName);
|
||||
|
||||
// Re-create or find SceneManager to ensure it matches the new runner
|
||||
var sceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>();
|
||||
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
|
||||
|
||||
var result = await _runner.StartGame(new StartGameArgs()
|
||||
{
|
||||
GameMode = GameMode.Host,
|
||||
SessionName = sessionName,
|
||||
SessionProperties = customProps,
|
||||
PlayerCount = 2,
|
||||
SceneManager = sceneManager
|
||||
});
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] StartHost SUCCESS");
|
||||
if (_runner.IsServer && _playerDataManagerPrefab.IsValid)
|
||||
{
|
||||
if (FindFirstObjectByType<PlayerDataManager>() == null)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Spawning PlayerDataManager");
|
||||
_runner.Spawn(_playerDataManagerPrefab, Vector3.zero, Quaternion.identity, null);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[BasicSpawner] Fusion StartHost Failed: {result.ShutdownReason}.");
|
||||
OnJoinFailedEvent?.Invoke();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StartClient(string sessionName, string password = null)
|
||||
{
|
||||
if (_isStarting) return false;
|
||||
_isStarting = true;
|
||||
|
||||
try
|
||||
{
|
||||
OnJoinStartedEvent?.Invoke();
|
||||
await EnsureRunnerExists();
|
||||
|
||||
var sceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>();
|
||||
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
|
||||
|
||||
var result = await _runner.StartGame(new StartGameArgs()
|
||||
{
|
||||
GameMode = GameMode.Client,
|
||||
SessionName = sessionName,
|
||||
SceneManager = sceneManager
|
||||
});
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[BasicSpawner] Fusion StartClient Failed: {result.ShutdownReason}");
|
||||
OnJoinFailedEvent?.Invoke();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();
|
||||
|
||||
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
|
||||
{
|
||||
Debug.Log($"[BasicSpawner] PlayerJoined: {player.PlayerId}");
|
||||
|
||||
// In Shared Mode, there is no Server. Each client is responsible for spawning their own player.
|
||||
if (player == runner.LocalPlayer)
|
||||
{
|
||||
SendLocalMetaData(player);
|
||||
SpawnPlayer(runner, player);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SendLocalMetaData(PlayerRef player)
|
||||
{
|
||||
PlayerDataManager pdm = null;
|
||||
int retries = 0;
|
||||
while (pdm == null && retries < 20)
|
||||
{
|
||||
pdm = FindFirstObjectByType<PlayerDataManager>();
|
||||
if (pdm != null) break;
|
||||
await Task.Delay(500);
|
||||
retries++;
|
||||
}
|
||||
|
||||
if (pdm != null)
|
||||
{
|
||||
string playerName = LocalPlayerProfile != null ? LocalPlayerProfile.Name : "Player " + player.PlayerId;
|
||||
|
||||
// Thêm hậu tố (HOST) nếu là server để dễ phân biệt
|
||||
if (_runner.IsServer) playerName += " (HOST)";
|
||||
|
||||
_Role playerRole = _Role.Seeker;
|
||||
|
||||
var metaData = new _PlayerMetaData()
|
||||
{
|
||||
Name = playerName,
|
||||
Role = playerRole,
|
||||
IsReady = false
|
||||
};
|
||||
pdm.RPC_UpdatePlayerMetaData(player, metaData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[BasicSpawner] Could not find PlayerDataManager after retries. Data will not sync.");
|
||||
}
|
||||
}
|
||||
|
||||
public void StartGame()
|
||||
{
|
||||
if (_runner != null && _runner.IsServer)
|
||||
{
|
||||
_runner.LoadScene("Main Scene");
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
|
||||
{
|
||||
if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
|
||||
{
|
||||
runner.Despawn(networkObject);
|
||||
_spawnedCharacters.Remove(player);
|
||||
}
|
||||
|
||||
// Logic Reassign Leader (Logical)
|
||||
if (runner.IsServer && PlayerDataManager.Instance != null && PlayerDataManager.Instance.Leader == player)
|
||||
{
|
||||
var nextLeader = runner.ActivePlayers.FirstOrDefault();
|
||||
if (nextLeader != PlayerRef.None)
|
||||
{
|
||||
PlayerDataManager.Instance.Leader = nextLeader;
|
||||
Debug.Log($"[BasicSpawner] Leader left. New logical leader: {nextLeader}");
|
||||
}
|
||||
}
|
||||
|
||||
if (runner.IsServer && player == runner.LocalPlayer)
|
||||
{
|
||||
runner.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason)
|
||||
{
|
||||
Debug.LogWarning($"[Fusion] Shutdown occurred. Reason: {shutdownReason}");
|
||||
OnShutdownEvent?.Invoke(shutdownReason.ToString());
|
||||
|
||||
// Nếu shutdown là do hệ thống chủ động hủy để tạo runner mới, KHÔNG quay về Menu
|
||||
if (_isInternalShutdown)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Internal shutdown detected, skipping Menu routing.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Nếu đang trong quá trình Host Migration, đừng quay về menu
|
||||
if (shutdownReason == ShutdownReason.HostMigration)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Shutdown due to Host Migration. Waiting for recovery...");
|
||||
return;
|
||||
}
|
||||
|
||||
/*if (UIManager.Instance != null)
|
||||
{
|
||||
UIManager.Instance.OnBackToMenu();
|
||||
}*/
|
||||
}
|
||||
|
||||
public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList)
|
||||
{
|
||||
OnSessionListUpdatedEvent?.Invoke(sessionList);
|
||||
}
|
||||
|
||||
public void OnInput(NetworkRunner runner, NetworkInput input)
|
||||
{
|
||||
var data = new PlayerInputData();
|
||||
if (Baba_yaga.Network.FusionClientMovementBridge.Local != null)
|
||||
{
|
||||
data = Baba_yaga.Network.FusionClientMovementBridge.Local.GetLocalInputData();
|
||||
}
|
||||
input.Set(data);
|
||||
}
|
||||
|
||||
public void OnConnectedToServer(NetworkRunner runner) { }
|
||||
public void OnDisconnectedFromServer(NetworkRunner runner, NetDisconnectReason reason) { }
|
||||
public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { }
|
||||
public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
|
||||
public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }
|
||||
public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, ArraySegment<byte> data) { }
|
||||
public void OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey key, float progress) { }
|
||||
public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }
|
||||
public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
|
||||
public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
|
||||
public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
|
||||
|
||||
public async void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] OnHostMigration triggered!");
|
||||
|
||||
// 1. Shutdown existing runner properly
|
||||
await runner.Shutdown(false);
|
||||
|
||||
// 2. Create new runner
|
||||
await EnsureRunnerExists();
|
||||
|
||||
// 3. Restart as new Host/Server using the migration token
|
||||
var result = await _runner.StartGame(new StartGameArgs()
|
||||
{
|
||||
HostMigrationToken = hostMigrationToken,
|
||||
SceneManager = gameObject.GetComponent<NetworkSceneManagerDefault>() ?? gameObject.AddComponent<NetworkSceneManagerDefault>()
|
||||
});
|
||||
|
||||
/*if (result.Ok)
|
||||
{
|
||||
Debug.Log("[BasicSpawner] Host Migration SUCCESSFUL");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[BasicSpawner] Host Migration FAILED: {result.ShutdownReason}");
|
||||
UIManager.Instance?.OnBackToMenu();
|
||||
}*/
|
||||
}
|
||||
|
||||
public void OnSceneLoadDone(NetworkRunner runner)
|
||||
{
|
||||
string currentSceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
|
||||
if (runner.IsServer && currentSceneName == "Main Scene")
|
||||
{
|
||||
foreach (var player in runner.ActivePlayers)
|
||||
{
|
||||
if (!_spawnedCharacters.ContainsKey(player))
|
||||
{
|
||||
SpawnPlayer(runner, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*if (currentSceneName == "Main Scene")
|
||||
{
|
||||
UIManager.Instance?.OnGameStarted();
|
||||
/
|
||||
BNM098TYU78I98IU7Y6T57U8I9I8U7Y6T57U8I7Y6T5Y67U8IU7Y6T57U8IU7Y6E4XDER45ESZXSDCER45EDSXZSDCEFR45TTRFGHJUIYTRW
|
||||
}*/
|
||||
}
|
||||
|
||||
private void SpawnPlayer(NetworkRunner runner, PlayerRef player)
|
||||
{
|
||||
Debug.Log($"[BasicSpawner] Spawning Player {player.PlayerId} at {Time.time}");
|
||||
Vector3 spawnPosition = (player == runner.LocalPlayer) ? new Vector3(-8, 2, 0) : new Vector3(8, 2, 0);
|
||||
var networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
|
||||
|
||||
// In Shared Mode, runner.Spawn automatically grants State Authority to the caller.
|
||||
// We just need to assign Input Authority.
|
||||
networkPlayerObject.AssignInputAuthority(player);
|
||||
|
||||
_spawnedCharacters[player] = networkPlayerObject;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (_runner != null && _runner.IsRunning)
|
||||
{
|
||||
GUI.color = Color.green;
|
||||
GUI.Label(new Rect(10, 10, 300, 30), $"[Network] Session: {_runner.SessionInfo?.Name}");
|
||||
GUI.Label(new Rect(10, 30, 300, 30), $"[Network] Players in Room: {_runner.ActivePlayers.Count()}");
|
||||
GUI.Label(new Rect(10, 50, 300, 30), $"[Network] Am I Server?: {_runner.IsServer}");
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSceneLoadStart(NetworkRunner runner) { }
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Network/BasicSpawner.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Network/BasicSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca752d01bdc2c5e42938776307031da3
|
||||
263
Assets/Scripts/Baba_yaga/Network/FusionClientMovementBridge.cs
Normal file
263
Assets/Scripts/Baba_yaga/Network/FusionClientMovementBridge.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using UnityEngine;
|
||||
using Fusion;
|
||||
using Opsive.Shared.Events;
|
||||
using Opsive.UltimateCharacterController.Networking;
|
||||
using Opsive.UltimateCharacterController.Networking.Character;
|
||||
using Opsive.UltimateCharacterController.Character;
|
||||
using Opsive.UltimateCharacterController.Character.Abilities;
|
||||
using Opsive.UltimateCharacterController.Character.Abilities.Items;
|
||||
using Opsive.UltimateCharacterController.Camera;
|
||||
using Opsive.UltimateCharacterController.Input;
|
||||
using Baba_yaga;
|
||||
using Opsive.UltimateCharacterController.Game;
|
||||
namespace Baba_yaga.Network
|
||||
{
|
||||
// Ensure Opsive components load before this
|
||||
[DefaultExecutionOrder(100)]
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Network", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class FusionClientMovementBridge : NetworkBehaviour, INetworkInfo, INetworkCharacter, ILookSource
|
||||
{
|
||||
public static FusionClientMovementBridge Local { get; private set; }
|
||||
|
||||
[Networked]
|
||||
public PlayerInputData SyncInput { get; set; }
|
||||
|
||||
private UltimateCharacterLocomotion m_CharacterLocomotion;
|
||||
private UltimateCharacterLocomotionHandler m_LocoHandler;
|
||||
private Opsive.UltimateCharacterController.Input.PlayerInput m_PlayerInput;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
m_CharacterLocomotion = GetComponent<UltimateCharacterLocomotion>();
|
||||
m_LocoHandler = GetComponent<UltimateCharacterLocomotionHandler>();
|
||||
m_PlayerInput = GetComponent<Opsive.UltimateCharacterController.Input.PlayerInput>();
|
||||
}
|
||||
|
||||
public override void Spawned()
|
||||
{
|
||||
// Because we passed State Authority to the client in BasicSpawner,
|
||||
// HasStateAuthority is true ONLY for the local player.
|
||||
// Check HasInputAuthority as well so the client's character correctly activates input!
|
||||
bool isLocal = Object.HasStateAuthority || Object.HasInputAuthority;
|
||||
|
||||
if (isLocal)
|
||||
{
|
||||
Local = this;
|
||||
}
|
||||
|
||||
// 1. Isolate Input: Only the local player should read keyboard/mouse inputs.
|
||||
if (m_LocoHandler != null) m_LocoHandler.enabled = isLocal;
|
||||
|
||||
var activeInput = GetComponent<UnityInput>(); // Corrected class reference
|
||||
if (activeInput != null) activeInput.enabled = isLocal;
|
||||
|
||||
// 2. Isolate Camera: Only attach the camera if this is the local player.
|
||||
if (isLocal)
|
||||
{
|
||||
var cameraController = UnityEngine.Object.FindFirstObjectByType<CameraController>();
|
||||
if (cameraController != null)
|
||||
{
|
||||
cameraController.Character = gameObject;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For remote players, register this bridge as the LookSource
|
||||
EventHandler.ExecuteEvent<ILookSource>(gameObject, "OnCharacterAttachLookSource", this);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Despawned(NetworkRunner runner, bool hasState)
|
||||
{
|
||||
if (Local == this)
|
||||
{
|
||||
Local = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void FixedUpdateNetwork()
|
||||
{
|
||||
// ONLY attempt to get input if we are the Host (StateAuthority) or the owning Client (InputAuthority).
|
||||
// Calling GetInput on a Proxy (someone else's character) is what causes the GetTypeKey exception!
|
||||
if (Object.HasStateAuthority || Object.HasInputAuthority)
|
||||
{
|
||||
if (GetInput<PlayerInputData>(out var input))
|
||||
{
|
||||
// Apply input locally so the Client's character actually moves predicted!
|
||||
if (m_CharacterLocomotion != null)
|
||||
{
|
||||
m_CharacterLocomotion.InputVector = input.InputVector;
|
||||
m_CharacterLocomotion.RawInputVector = input.RawInputVector;
|
||||
m_CharacterLocomotion.DeltaRotation = input.DeltaRotation;
|
||||
}
|
||||
|
||||
if (Object.HasStateAuthority)
|
||||
{
|
||||
SyncInput = input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we do NOT have StateAuthority AND we do NOT have InputAuthority, it's a Proxy (remote player).
|
||||
if (Object.IsProxy)
|
||||
{
|
||||
// Ensure look source is attached for remote players
|
||||
if (m_CharacterLocomotion != null && m_CharacterLocomotion.LookSource != (ILookSource)this)
|
||||
{
|
||||
EventHandler.ExecuteEvent<ILookSource>(gameObject, "OnCharacterAttachLookSource", this);
|
||||
}
|
||||
|
||||
// Sync the movement inputs to KinematicObjectManager so it moves the remote player character
|
||||
if (m_CharacterLocomotion != null && m_CharacterLocomotion.KinematicObjectIndex != -1)
|
||||
{
|
||||
KinematicObjectManager.SetCharacterMovementInput(
|
||||
m_CharacterLocomotion.KinematicObjectIndex,
|
||||
SyncInput.Direction.x,
|
||||
SyncInput.Direction.y
|
||||
);
|
||||
}
|
||||
|
||||
if (m_CharacterLocomotion != null)
|
||||
{
|
||||
m_CharacterLocomotion.InputVector = SyncInput.InputVector;
|
||||
m_CharacterLocomotion.RawInputVector = SyncInput.RawInputVector;
|
||||
m_CharacterLocomotion.DeltaRotation = SyncInput.DeltaRotation;
|
||||
|
||||
// Sync remote abilities based on state
|
||||
UpdateRemoteAbility<SpeedChange>(SyncInput.sprint);
|
||||
UpdateRemoteAbility<Jump>(SyncInput.jump);
|
||||
UpdateRemoteAbility<HeightChange>(SyncInput.crouch);
|
||||
UpdateRemoteAbility<Aim>(SyncInput.aim);
|
||||
UpdateRemoteAbility<Use>(SyncInput.use);
|
||||
UpdateRemoteAbility<Reload>(SyncInput.reload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRemoteAbility<T>(bool shouldBeActive) where T : Ability
|
||||
{
|
||||
if (m_CharacterLocomotion == null) return;
|
||||
var ability = m_CharacterLocomotion.GetAbility<T>();
|
||||
if (ability != null)
|
||||
{
|
||||
if (shouldBeActive && !ability.IsActive)
|
||||
{
|
||||
m_CharacterLocomotion.TryStartAbility(ability, true);
|
||||
}
|
||||
else if (!shouldBeActive && ability.IsActive)
|
||||
{
|
||||
m_CharacterLocomotion.TryStopAbility(ability, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerInputData GetLocalInputData()
|
||||
{
|
||||
var data = new PlayerInputData();
|
||||
if (m_CharacterLocomotion == null) return data;
|
||||
|
||||
if (m_PlayerInput != null)
|
||||
{
|
||||
data.Direction = new Vector2(m_PlayerInput.GetAxisRaw("Horizontal"), m_PlayerInput.GetAxisRaw("Vertical"));
|
||||
|
||||
// Sync abilities active states or buttons
|
||||
data.sprint = IsAbilityActive<SpeedChange>() || m_PlayerInput.GetButton("Change Speeds");
|
||||
data.jump = IsAbilityActive<Jump>() || m_PlayerInput.GetButton("Jump");
|
||||
data.crouch = IsAbilityActive<HeightChange>() || m_PlayerInput.GetButton("Crouch");
|
||||
data.aim = IsAbilityActive<Aim>() || m_PlayerInput.GetButton("Aim");
|
||||
data.use = IsAbilityActive<Use>() || m_PlayerInput.GetButton("Fire1");
|
||||
data.reload = IsAbilityActive<Reload>() || m_PlayerInput.GetButton("Reload");
|
||||
}
|
||||
|
||||
// Sync locomotion internal state parameters
|
||||
data.InputVector = m_CharacterLocomotion.InputVector;
|
||||
data.RawInputVector = m_CharacterLocomotion.RawInputVector;
|
||||
data.DeltaRotation = m_CharacterLocomotion.DeltaRotation;
|
||||
data.rot = transform.rotation;
|
||||
|
||||
// Sync Look direction and look pitch
|
||||
var lookSource = m_CharacterLocomotion.LookSource;
|
||||
if (lookSource != null)
|
||||
{
|
||||
data.LookPitch = lookSource.Pitch;
|
||||
data.LookDirection = lookSource.LookDirection(true);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private bool IsAbilityActive<T>() where T : Ability
|
||||
{
|
||||
if (m_CharacterLocomotion == null) return false;
|
||||
var ability = m_CharacterLocomotion.GetAbility<T>();
|
||||
return ability != null && ability.IsActive;
|
||||
}
|
||||
|
||||
// --- ILookSource Implementation ---
|
||||
public GameObject GameObject => gameObject;
|
||||
public Transform Transform => transform;
|
||||
public float LookDirectionDistance => 1f;
|
||||
public float Pitch => SyncInput.LookPitch;
|
||||
|
||||
public Vector3 LookPosition()
|
||||
{
|
||||
var animator = GetComponent<Animator>();
|
||||
if (animator != null)
|
||||
{
|
||||
var head = animator.GetBoneTransform(HumanBodyBones.Head);
|
||||
if (head != null) return head.position;
|
||||
}
|
||||
return transform.position + Vector3.up * 1.5f;
|
||||
}
|
||||
|
||||
public Vector3 LookDirection(bool characterLookDirection)
|
||||
{
|
||||
return SyncInput.LookDirection == Vector3.zero ? transform.forward : SyncInput.LookDirection;
|
||||
}
|
||||
|
||||
public Vector3 LookDirection(Vector3 lookPosition, bool characterLookDirection, int layerMask, bool useRecoil)
|
||||
{
|
||||
return LookDirection(characterLookDirection);
|
||||
}
|
||||
|
||||
// --- INetworkInfo Implementation ---
|
||||
|
||||
public bool IsLocalPlayer() => Object.HasStateAuthority || Object.HasInputAuthority;
|
||||
public bool IsServer() => Runner.IsServer;
|
||||
|
||||
// Return FALSE because we are doing Client-Authoritative movement!
|
||||
public bool IsServerAuthoritative() => false;
|
||||
|
||||
|
||||
// --- INetworkCharacter Implementation ---
|
||||
// Since we are using Fusion's NetworkTransform to sync position,
|
||||
// we can leave these methods empty. Opsive will handle the local
|
||||
// movement, and Fusion's NetworkTransform will drag the remote players.
|
||||
|
||||
public void SetPosition(Vector3 position, bool snapAnimator) { }
|
||||
public void SetRotation(Quaternion rotation, bool snapAnimator) { }
|
||||
public void SetPositionAndRotation(Vector3 position, Quaternion rotation, bool snapAnimator) { }
|
||||
public void ResetRotationPosition() { }
|
||||
public void SetActive(bool active, bool uiEvent) { }
|
||||
|
||||
public void LoadDefaultLoadout() { }
|
||||
public void EquipUnequipItem(uint itemID, int slotID, bool equip) { }
|
||||
public void ItemIdentifierPickup(uint id, int amount, int slot, bool immediate, bool force) { }
|
||||
public void RemoveAllItems() { }
|
||||
|
||||
public void Fire(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, float strength) { }
|
||||
public void StartItemReload(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction) { }
|
||||
public void ReloadItem(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, bool fullClip) { }
|
||||
public void ItemReloadComplete(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, bool success, bool immediateReload) { }
|
||||
public void MeleeHitCollider(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, int hitboxIndex, RaycastHit raycastHit, GameObject hitGameObject, UltimateCharacterLocomotion hitCharacterLocomotion) { }
|
||||
|
||||
public void ThrowItem(Opsive.UltimateCharacterController.Items.Actions.ItemAction action) { }
|
||||
public void EnableThrowableObjectMeshRenderers(Opsive.UltimateCharacterController.Items.Actions.ItemAction action) { }
|
||||
public void StartStopBeginEndMagicActions(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, bool begin, bool start) { }
|
||||
public void MagicCast(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, int index, uint castID, Vector3 dir, Vector3 target) { }
|
||||
public void MagicImpact(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, uint castID, GameObject source, GameObject target, Vector3 pos, Vector3 norm) { }
|
||||
public void StopMagicCast(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, int index, uint castID) { }
|
||||
public void ToggleFlashlight(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, bool active) { }
|
||||
public void PushRigidbody(Rigidbody rb, Vector3 force, Vector3 point) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39343fbd597ed5947b34fa2777fa1b7c
|
||||
74
Assets/Scripts/Baba_yaga/Network/MatchResultManager.cs
Normal file
74
Assets/Scripts/Baba_yaga/Network/MatchResultManager.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Fusion;
|
||||
using UnityEngine;
|
||||
using Baba_yaga.Game;
|
||||
using Baba_yaga.UI;
|
||||
using System.Linq;
|
||||
|
||||
namespace Baba_yaga.Network
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.Network", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class MatchResultManager : NetworkBehaviour
|
||||
{
|
||||
public static MatchResultManager Instance { get; private set; }
|
||||
|
||||
public override void Spawned()
|
||||
{
|
||||
if (Object.HasStateAuthority) Instance = this;
|
||||
}
|
||||
|
||||
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
|
||||
public void RPC_BroadcastResult(PlayerRef winner, EloResult eloResult)
|
||||
{
|
||||
Debug.Log($"Game Over! Winner: {winner}. Elo updated.");
|
||||
|
||||
// Update local Elo display and show Result UI
|
||||
if (Runner.LocalPlayer == winner)
|
||||
{
|
||||
ShowResultUI(true, eloResult.DeltaA, eloResult.NewRatingA);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowResultUI(false, eloResult.DeltaB, eloResult.NewRatingB);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowResultUI(bool isWin, int delta, int newRating)
|
||||
{
|
||||
// In a real scenario, we might push a new Result screen
|
||||
// For now, let's assume HUD has a result panel
|
||||
Debug.Log($"RESULT: {(isWin ? "WIN" : "LOSS")} | Delta: {delta} | New Rating: {newRating}");
|
||||
|
||||
// Save to PlayerPrefs as a dummy "Server" persistence
|
||||
PlayerPrefs.SetInt("EloRating", newRating);
|
||||
int gamesPlayed = PlayerPrefs.GetInt("GamesPlayed", 0);
|
||||
PlayerPrefs.SetInt("GamesPlayed", gamesPlayed + 1);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public void ProcessMatchEnd(PlayerRef winner)
|
||||
{
|
||||
if (!Object.HasStateAuthority) return;
|
||||
|
||||
// Get ratings for both players
|
||||
// In a real game, these would come from the server/metadata
|
||||
int ratingA = PlayerPrefs.GetInt("EloRating", 1000);
|
||||
int ratingB = 1000; // Placeholder for opponent
|
||||
int gamesA = PlayerPrefs.GetInt("GamesPlayed", 0);
|
||||
int gamesB = 0; // Placeholder
|
||||
|
||||
float resultA = (Runner.LocalPlayer == winner) ? 1.0f : 0.0f;
|
||||
|
||||
EloResult elo = EloSystem.Calculate(ratingA, ratingB, gamesA, gamesB, resultA);
|
||||
|
||||
RPC_BroadcastResult(winner, elo);
|
||||
|
||||
// Shut down runner after some delay
|
||||
Invoke(nameof(ShutdownRunner), 5.0f);
|
||||
}
|
||||
|
||||
private void ShutdownRunner()
|
||||
{
|
||||
Runner.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9eac0255d30a2bb40a43ff12cdcdf960
|
||||
30
Assets/Scripts/Baba_yaga/Network/PlayerData.cs
Normal file
30
Assets/Scripts/Baba_yaga/Network/PlayerData.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Fusion;
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerData : NetworkBehaviour
|
||||
{
|
||||
[Networked]
|
||||
public _Role PlayerRole { get; set; }
|
||||
|
||||
public override void Spawned()
|
||||
{
|
||||
if (Object.HasInputAuthority)
|
||||
{
|
||||
SetupByRole(PlayerRole);
|
||||
}
|
||||
}
|
||||
|
||||
void SetupByRole(_Role role)
|
||||
{
|
||||
if (role == _Role.Seeker)
|
||||
{
|
||||
Debug.Log("I am Seeker");
|
||||
// bật flashlight
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("I am Trapper");
|
||||
// bật trap UI
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Network/PlayerData.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Network/PlayerData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96ce77b74a34e7440a0b54af32c6d402
|
||||
80
Assets/Scripts/Baba_yaga/Network/PlayerDataManager.cs
Normal file
80
Assets/Scripts/Baba_yaga/Network/PlayerDataManager.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using Fusion;
|
||||
using UnityEngine;
|
||||
|
||||
// struct quản lý thông tin
|
||||
public struct _PlayerMetaData : INetworkStruct
|
||||
{
|
||||
public NetworkString<_16> Name;
|
||||
public _Role Role;
|
||||
public NetworkBool IsReady;
|
||||
}
|
||||
|
||||
public class PlayerDataManager : NetworkBehaviour
|
||||
{
|
||||
public static PlayerDataManager Instance { get; private set; }
|
||||
|
||||
[Networked]
|
||||
public NetworkDictionary<PlayerRef, _PlayerMetaData> Players => default;
|
||||
|
||||
[Networked]
|
||||
public PlayerRef Leader { get; set; }
|
||||
|
||||
public event Action<PlayerRef, string> OnChatMessageReceived;
|
||||
|
||||
public override void Spawned()
|
||||
{
|
||||
Instance = this;
|
||||
if (Object.HasStateAuthority)
|
||||
{
|
||||
Leader = Runner.LocalPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Despawned(NetworkRunner runner, bool hasState)
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
|
||||
public void RPC_TransferLeader(PlayerRef newLeader)
|
||||
{
|
||||
if (Players.ContainsKey(newLeader))
|
||||
{
|
||||
Leader = newLeader;
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
|
||||
public void RPC_UpdatePlayerMetaData(PlayerRef playerRef, _PlayerMetaData metaData)
|
||||
{
|
||||
if (Object == null || !Object.IsValid) return;
|
||||
Players.Set(playerRef, metaData);
|
||||
}
|
||||
|
||||
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
|
||||
public void RPC_SetReady(PlayerRef playerRef, bool ready)
|
||||
{
|
||||
if (Object == null || !Object.IsValid) return;
|
||||
if (Players.TryGet(playerRef, out var data))
|
||||
{
|
||||
data.IsReady = ready;
|
||||
Players.Set(playerRef, data);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(RpcSources.All, RpcTargets.All)]
|
||||
public void RPC_SendChatMessage(PlayerRef sender, string message)
|
||||
{
|
||||
OnChatMessageReceived?.Invoke(sender, message);
|
||||
}
|
||||
|
||||
public bool TryGetPlayerMetaData(PlayerRef playerRef, out _PlayerMetaData metaData)
|
||||
{
|
||||
metaData = default;
|
||||
// Kiểm tra xem object đã được Spawned chưa trước khi truy cập networked property
|
||||
if (Object == null || !Object.IsValid) return false;
|
||||
|
||||
return Players.TryGet(playerRef, out metaData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3d9934ebd60c9c4ea3e464b77fd7ae0
|
||||
56
Assets/Scripts/Baba_yaga/Network/PlayerInfo.cs
Normal file
56
Assets/Scripts/Baba_yaga/Network/PlayerInfo.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Fusion;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
public enum _Role
|
||||
{
|
||||
Seeker,
|
||||
Trapper
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class PlayerProfile
|
||||
{
|
||||
public string Name = "Player";
|
||||
public _Role Role = _Role.Seeker;
|
||||
}
|
||||
|
||||
public class PlayerInfo : NetworkBehaviour
|
||||
{
|
||||
[Networked] public string playerName { get; set; }
|
||||
|
||||
public PlayerDataManager playerDataManager;
|
||||
public TextMeshProUGUI nameText;
|
||||
|
||||
public GameObject[] characterIcons; // mảng chứa icon tương ứng với từng class, có thể gán trong inspector
|
||||
|
||||
// sau khi game object được tạo ra trên mạng,
|
||||
// sẽ gọi phương thức này để khởi tạo thông tin player
|
||||
public override void Spawned()
|
||||
{
|
||||
playerDataManager = FindFirstObjectByType<PlayerDataManager>(); // tìm PlayerDataManager trong scene
|
||||
}
|
||||
|
||||
// phương thức này sẽ được gọi mỗi frame để cập nhật thông tin hiển thị của player
|
||||
public override void Render()
|
||||
{
|
||||
if (playerDataManager == null) return;
|
||||
if (playerDataManager.TryGetPlayerMetaData(Object.InputAuthority, out var metadata))
|
||||
{
|
||||
var name = metadata.Name;
|
||||
var charClass = metadata.Role;
|
||||
|
||||
if (nameText != null)
|
||||
nameText.text = $"{name} ({charClass})";
|
||||
|
||||
if (characterIcons != null)
|
||||
{
|
||||
for (var i = 0; i < characterIcons.Length; i++)
|
||||
{
|
||||
if (characterIcons[i] != null)
|
||||
characterIcons[i].SetActive(i == (int)charClass); // hiển thị icon tương ứng với class của player
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Network/PlayerInfo.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Network/PlayerInfo.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70abb536cf50f2948882e913634daedf
|
||||
49
Assets/Scripts/Baba_yaga/Network/PlayerInputData.cs
Normal file
49
Assets/Scripts/Baba_yaga/Network/PlayerInputData.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
using Fusion;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Baba_yaga
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "OnlyScove.Scripts", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public struct PlayerInputData : INetworkInput, INetworkStruct
|
||||
{
|
||||
// Di chuyển (thường là Vector2 cho X/Y hoặc WASD)
|
||||
public Vector2 Direction;
|
||||
|
||||
// Trạng thái chạy nhanh
|
||||
public NetworkBool sprint;
|
||||
|
||||
// Trạng thái nhảy
|
||||
public NetworkBool jump;
|
||||
|
||||
// Góc quay của Camera/Nhân vật (Dùng Quaternion hoặc Vector3 tùy thuộc vào PlanarRotation của bạn)
|
||||
public Quaternion rot;
|
||||
|
||||
// Vector di chuyển đã qua xử lý (UCC CharacterLocomotion.InputVector)
|
||||
public Vector2 InputVector;
|
||||
|
||||
// Vector di chuyển thô (UCC UltimateCharacterLocomotion.RawInputVector)
|
||||
public Vector2 RawInputVector;
|
||||
|
||||
// Độ lệch xoay của nhân vật (UCC CharacterLocomotion.DeltaRotation)
|
||||
public Vector3 DeltaRotation;
|
||||
|
||||
// Trạng thái ngồi (UCC HeightChange ability)
|
||||
public NetworkBool crouch;
|
||||
|
||||
// Trạng thái ngắm bắn (UCC Aim ability)
|
||||
public NetworkBool aim;
|
||||
|
||||
// Trạng thái sử dụng item/tấn công (UCC Use ability)
|
||||
public NetworkBool use;
|
||||
|
||||
// Trạng thái thay đạn (UCC Reload ability)
|
||||
public NetworkBool reload;
|
||||
|
||||
// Góc Pitch của nguồn nhìn (UCC ILookSource.Pitch)
|
||||
public float LookPitch;
|
||||
|
||||
// Hướng nhìn của nhân vật (UCC ILookSource.LookDirection)
|
||||
public Vector3 LookDirection;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Network/PlayerInputData.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Network/PlayerInputData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 731ed4a4b6e0ae64c8194463a76646c7
|
||||
3
Assets/Scripts/Baba_yaga/Others.meta
Normal file
3
Assets/Scripts/Baba_yaga/Others.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ddac8c6596e46dfaec8a37379dbfeec
|
||||
timeCreated: 1780542076
|
||||
8
Assets/Scripts/Baba_yaga/Others/StickyNote.cs
Normal file
8
Assets/Scripts/Baba_yaga/Others/StickyNote.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class StickyNote : MonoBehaviour
|
||||
{
|
||||
[TextArea] public string noteText = "Enter note here...";
|
||||
public Color noteColor = Color.yellow;
|
||||
public bool showAlways = true; // Show even when not selected
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Others/StickyNote.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Others/StickyNote.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20733ff6cdef89e408acddf8ce51503d
|
||||
8
Assets/Scripts/Baba_yaga/Player.meta
Normal file
8
Assets/Scripts/Baba_yaga/Player.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e0fd09e39d1b90458b7097e555a9f3f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Baba_yaga/Trap.meta
Normal file
8
Assets/Scripts/Baba_yaga/Trap.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82bfa2ea8f45df942b2e9d2855a5ff6e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Assets/Scripts/Baba_yaga/Trap/TeleportTrap.cs
Normal file
49
Assets/Scripts/Baba_yaga/Trap/TeleportTrap.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
public class TeleportTrap : MonoBehaviour {
|
||||
|
||||
|
||||
private Vector3 GetRandomPosition() {
|
||||
Vector3 randomPosition;
|
||||
int attempts = 0;
|
||||
|
||||
do {
|
||||
// Generate random X and Z positions rounded to nearest 10, then offset by 5
|
||||
float x = Mathf.Round(Random.Range(-45f, 45f) / 10f) * 10f + 5f;
|
||||
float z = Mathf.Round(Random.Range(-45f, 45f) / 10f) * 10f + 5f;
|
||||
|
||||
// Fixed Y position for traps
|
||||
randomPosition = new Vector3(x, 3.5f, z);
|
||||
|
||||
attempts++;
|
||||
if (attempts > 100) {
|
||||
Debug.LogWarning("No valid position found for the death trap.");
|
||||
|
||||
// Exit if too many attempts are made
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (
|
||||
Physics.CheckBox(randomPosition, new Vector3(2.5f, 3.5f, 2.5f), Quaternion.identity, LayerMask.GetMask("Walls")) || // Check for walls
|
||||
Physics.CheckBox(randomPosition, new Vector3(2.5f, 3.5f, 2.5f), Quaternion.identity, LayerMask.GetMask("Collectible")) || // Check for collectibles
|
||||
Physics.CheckBox(randomPosition, new Vector3(2.5f, 3.5f, 2.5f), Quaternion.identity, LayerMask.GetMask("Player")) // Check for player
|
||||
);
|
||||
|
||||
// Return a valid position for the death trap
|
||||
return randomPosition;
|
||||
}
|
||||
|
||||
private Vector3 GetPositionTeleport() {
|
||||
Vector3 spawnPosition = GetRandomPosition();
|
||||
return spawnPosition;
|
||||
}
|
||||
|
||||
private void OnCollisionEnter(Collision collision)
|
||||
{
|
||||
if (collision.gameObject.CompareTag("Player"))
|
||||
{
|
||||
collision.transform.position = GetPositionTeleport();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Baba_yaga/Trap/TeleportTrap.cs.meta
Normal file
2
Assets/Scripts/Baba_yaga/Trap/TeleportTrap.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca1a9a3813a058946990c84846f54c17
|
||||
8
Assets/Scripts/Baba_yaga/Tuấn.meta
Normal file
8
Assets/Scripts/Baba_yaga/Tuấn.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ce3d3ef991b9c34baf5cdc7e782f909
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Scripts/Baba_yaga/Tuấn/Plan.txt
Normal file
33
Assets/Scripts/Baba_yaga/Tuấn/Plan.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
1. Hệ thống Core (Dễ)
|
||||
* Mục tiêu: Xây dựng nền tảng cho mọi vật thể tương tác.
|
||||
* Các bước:
|
||||
* Hoàn thiện Interface IInteractable và lớp trừu tượng BaseInteractable.
|
||||
* Tạo script PlayerInteraction sử dụng Raycast từ Camera để phát hiện vật thể và nhận lệnh nhấn phím (ví dụ: phím E).
|
||||
|
||||
2. UI Prompt & Âm thanh (Dễ)
|
||||
* Mục tiêu: Phản hồi trực quan và âm thanh cho người chơi.
|
||||
* Các bước:
|
||||
* Thêm thành phần hiển thị Text vào HUD (sử dụng UIToolkit như các UI hiện tại của bạn).
|
||||
* Tích hợp âm thanh từ dữ liệu ObjectInteraction (ScriptableObject) vào hàm tương tác cơ bản.
|
||||
|
||||
3. Highlight & Tương tác vật lý (Trung bình)
|
||||
* Mục tiêu: Làm nổi bật vật thể và thực hiện các tương tác đóng/mở.
|
||||
* Các bước:
|
||||
* Object Highlight: Sử dụng Outline shader hoặc thay đổi Material khi người chơi nhìn vào vật thể.
|
||||
* Door Interaction: Hoàn thiện logic mở/đóng cửa (kết nối với Animator hoặc script có sẵn).
|
||||
* Lever/Switch: Tạo hệ thống cần gạt sử dụng UnityEvent để kích hoạt các sự kiện khác trong màn chơi.
|
||||
|
||||
4. Tương tác đặc biệt & Logic (Khó)
|
||||
* Mục tiêu: Các cơ chế gameplay phức tạp hơn.
|
||||
* Các bước:
|
||||
* Fake Wall: Tường giả biến mất hoặc cho phép đi xuyên qua.
|
||||
* Teleport: Dịch chuyển người chơi đến vị trí chỉ định.
|
||||
* Exit: Xử lý chuyển Scene hoặc kết thúc màn chơi.
|
||||
* Mirror: Hiệu ứng gương phản chiếu và logic tương tác riêng.
|
||||
|
||||
5. Đồng bộ Online & Kiểm thử (Cực khó)
|
||||
* Mục tiêu: Đảm bảo hệ thống hoạt động trong môi trường Multiplayer.
|
||||
* Các bước:
|
||||
* Sử dụng Photon (RPC) để đồng bộ trạng thái vật thể (ví dụ: khi một người mở cửa, tất cả mọi người đều thấy cửa mở).
|
||||
Cách thức triển khai: Chúng ta sẽ đi theo mô hình Modular. Tôi sẽ xây dựng hệ thống Core trước, sau đó mỗi loại vật thể (Cửa, Cần gạt, Tường giả...) sẽ là một Module riêng kế thừa từ Core. Điều này giúp code của bạn sạch, dễ
|
||||
quản lý và dễ mở rộng sau này.
|
||||
7
Assets/Scripts/Baba_yaga/Tuấn/Plan.txt.meta
Normal file
7
Assets/Scripts/Baba_yaga/Tuấn/Plan.txt.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54fd09d52f600024291972ed9edfcc52
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Scripts/Baba_yaga/UI.meta
Normal file
3
Assets/Scripts/Baba_yaga/UI.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91b95b0bf23143b68483f912b558e6f0
|
||||
timeCreated: 1773383929
|
||||
8
Assets/Scripts/Baba_yaga/UI/Components.meta
Normal file
8
Assets/Scripts/Baba_yaga/UI/Components.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50f4c7bcd8223714fa716961ca9e3688
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
135
Assets/Scripts/Baba_yaga/UI/Components/VectorShapeElement.cs
Normal file
135
Assets/Scripts/Baba_yaga/UI/Components/VectorShapeElement.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Baba_yaga.UI.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// An adjustable Vector Element that you can "sketch" and tweak directly in UI Builder (UXML).
|
||||
/// Supports Parametric shapes (Pill, Polygon, Star) and Custom Paths.
|
||||
/// </summary>
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.UI.Components", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public class VectorShapeElement : VisualElement
|
||||
{
|
||||
[UnityEngine.Scripting.APIUpdating.MovedFrom(true, sourceNamespace: "Hallucinate.UI.Components", sourceAssembly: "Opsive.UltimateCharacterController")]
|
||||
public enum ShapeType { Pill, Polygon, Star, CustomPath }
|
||||
|
||||
public new class UxmlFactory : UxmlFactory<VectorShapeElement, UxmlTraits> { }
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
UxmlEnumAttributeDescription<ShapeType> m_ShapeType = new UxmlEnumAttributeDescription<ShapeType> { name = "shape-type", defaultValue = ShapeType.Pill };
|
||||
UxmlColorAttributeDescription m_FillColor = new UxmlColorAttributeDescription { name = "fill-color", defaultValue = Color.white };
|
||||
UxmlColorAttributeDescription m_StrokeColor = new UxmlColorAttributeDescription { name = "stroke-color", defaultValue = Color.black };
|
||||
UxmlFloatAttributeDescription m_StrokeWidth = new UxmlFloatAttributeDescription { name = "stroke-width", defaultValue = 2f };
|
||||
UxmlFloatAttributeDescription m_CornerRadius = new UxmlFloatAttributeDescription { name = "corner-radius", defaultValue = 10f };
|
||||
UxmlIntAttributeDescription m_Sides = new UxmlIntAttributeDescription { name = "sides", defaultValue = 5 };
|
||||
UxmlFloatAttributeDescription m_Inwardness = new UxmlFloatAttributeDescription { name = "star-inwardness", defaultValue = 0.5f };
|
||||
UxmlStringAttributeDescription m_PathData = new UxmlStringAttributeDescription { name = "path-data", defaultValue = "" };
|
||||
|
||||
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
|
||||
{
|
||||
base.Init(ve, bag, cc);
|
||||
var ate = ve as VectorShapeElement;
|
||||
|
||||
ate.shapeType = m_ShapeType.GetValueFromBag(bag, cc);
|
||||
ate.fillColor = m_FillColor.GetValueFromBag(bag, cc);
|
||||
ate.strokeColor = m_StrokeColor.GetValueFromBag(bag, cc);
|
||||
ate.strokeWidth = m_StrokeWidth.GetValueFromBag(bag, cc);
|
||||
ate.cornerRadius = m_CornerRadius.GetValueFromBag(bag, cc);
|
||||
ate.sides = Mathf.Max(3, m_Sides.GetValueFromBag(bag, cc));
|
||||
ate.inwardness = Mathf.Clamp01(m_Inwardness.GetValueFromBag(bag, cc));
|
||||
ate.pathData = m_PathData.GetValueFromBag(bag, cc);
|
||||
|
||||
ate.MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
public ShapeType shapeType { get; set; }
|
||||
public Color fillColor { get; set; }
|
||||
public Color strokeColor { get; set; }
|
||||
public float strokeWidth { get; set; }
|
||||
public float cornerRadius { get; set; }
|
||||
public int sides { get; set; }
|
||||
public float inwardness { get; set; }
|
||||
public string pathData { get; set; }
|
||||
|
||||
public VectorShapeElement()
|
||||
{
|
||||
generateVisualContent += OnGenerateVisualContent;
|
||||
}
|
||||
|
||||
private void OnGenerateVisualContent(MeshGenerationContext mgc)
|
||||
{
|
||||
var paint = mgc.painter2D;
|
||||
var rect = contentRect;
|
||||
|
||||
if (rect.width <= 0 || rect.height <= 0) return;
|
||||
|
||||
paint.BeginPath();
|
||||
paint.fillColor = fillColor;
|
||||
paint.strokeColor = strokeColor;
|
||||
paint.lineWidth = strokeWidth;
|
||||
|
||||
switch (shapeType)
|
||||
{
|
||||
case ShapeType.Pill: DrawPill(paint, rect); break;
|
||||
case ShapeType.Polygon: DrawPolygon(paint, rect, false); break;
|
||||
case ShapeType.Star: DrawPolygon(paint, rect, true); break;
|
||||
case ShapeType.CustomPath: DrawCustomPath(paint); break;
|
||||
}
|
||||
|
||||
paint.ClosePath();
|
||||
paint.Fill();
|
||||
if (strokeWidth > 0) paint.Stroke();
|
||||
}
|
||||
|
||||
private void DrawPill(Painter2D paint, Rect rect)
|
||||
{
|
||||
float r = Mathf.Min(cornerRadius, rect.width / 2f, rect.height / 2f);
|
||||
paint.MoveTo(new Vector2(r, 0));
|
||||
paint.LineTo(new Vector2(rect.width - r, 0));
|
||||
paint.ArcTo(new Vector2(rect.width, 0), new Vector2(rect.width, r), r);
|
||||
paint.LineTo(new Vector2(rect.width, rect.height - r));
|
||||
paint.ArcTo(new Vector2(rect.width, rect.height), new Vector2(rect.width - r, rect.height), r);
|
||||
paint.LineTo(new Vector2(r, rect.height));
|
||||
paint.ArcTo(new Vector2(0, rect.height), new Vector2(0, rect.height - r), r);
|
||||
paint.LineTo(new Vector2(0, r));
|
||||
paint.ArcTo(new Vector2(0, 0), new Vector2(r, 0), r);
|
||||
}
|
||||
|
||||
private void DrawPolygon(Painter2D paint, Rect rect, bool isStar)
|
||||
{
|
||||
Vector2 center = rect.center;
|
||||
float radius = Mathf.Min(rect.width, rect.height) / 2f;
|
||||
int totalPoints = isStar ? sides * 2 : sides;
|
||||
float angleStep = 360f / totalPoints;
|
||||
|
||||
for (int i = 0; i < totalPoints; i++)
|
||||
{
|
||||
float angle = (i * angleStep - 90f) * Mathf.Deg2Rad;
|
||||
float r = radius;
|
||||
if (isStar && i % 2 != 0) r *= inwardness;
|
||||
|
||||
Vector2 pos = center + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * r;
|
||||
if (i == 0) paint.MoveTo(pos);
|
||||
else paint.LineTo(pos);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCustomPath(Painter2D paint)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pathData)) return;
|
||||
string[] pairs = pathData.Split(' ');
|
||||
for (int i = 0; i < pairs.Length; i++)
|
||||
{
|
||||
string[] coords = pairs[i].Split(',');
|
||||
if (coords.Length == 2 && float.TryParse(coords[0], out float x) && float.TryParse(coords[1], out float y))
|
||||
{
|
||||
if (i == 0) paint.MoveTo(new Vector2(x, y));
|
||||
else paint.LineTo(new Vector2(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b988e1760186e3f4d8c8041cbc6d64a2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user