Organize custom scripts and Shared under Assets/Scripts, and delete assembly definition files
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user