Added map and fixed other bugs
This commit is contained in:
@@ -14,7 +14,14 @@ public class GhostPlayer : NetworkBehaviour
|
||||
[SerializeField] private GameObject hitEffectPrefab;
|
||||
|
||||
[Header("Visuals")]
|
||||
[SerializeField] private Color ghostColor = new Color(0.5f, 0.5f, 0.5f, 1f);
|
||||
[Tooltip("Тусклый цвет СВОЕГО клона (клон локального игрока)")]
|
||||
[SerializeField] private Color ownGhostColor = new Color(0f, 0f, 0.5f, 1f);
|
||||
[Tooltip("Тусклый цвет ЧУЖОГО клона (клон соперника)")]
|
||||
[SerializeField] private Color enemyGhostColor = new Color(0.5f, 0f, 0f, 1f);
|
||||
|
||||
// ClientId оригинального игрока. Синхронизируется на клиентов для окраски
|
||||
// (recording живёт только на сервере, поэтому цвет по нему клиент не узнает).
|
||||
private NetworkVariable<ulong> ownerId = new NetworkVariable<ulong>(ulong.MaxValue);
|
||||
|
||||
private GhostRecording recording;
|
||||
private int shotIdx;
|
||||
@@ -22,15 +29,34 @@ public class GhostPlayer : NetworkBehaviour
|
||||
|
||||
private Collider[] ownColliders;
|
||||
private PlayerHealth health;
|
||||
private PlayerShield shield;
|
||||
|
||||
/// <summary> Кому засчитывать вход в яму / фраги — ClientId оригинального игрока. </summary>
|
||||
public ulong OriginalOwnerId => recording != null ? recording.ownerClientId : ulong.MaxValue;
|
||||
public ulong OriginalOwnerId => ownerId.Value;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// Красим клона на всех клиентах, чтобы отличался от живых игроков
|
||||
ownerId.OnValueChanged += OnOwnerChanged;
|
||||
ApplyColor();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
ownerId.OnValueChanged -= OnOwnerChanged;
|
||||
}
|
||||
|
||||
private void OnOwnerChanged(ulong _previous, ulong _current) => ApplyColor();
|
||||
|
||||
// Свой клон — тусклый синий, чужой — тусклый красный (как живые игроки, но тускло)
|
||||
private void ApplyColor()
|
||||
{
|
||||
bool _isMine = NetworkManager.Singleton != null
|
||||
&& ownerId.Value == NetworkManager.Singleton.LocalClientId;
|
||||
|
||||
Color _color = _isMine ? ownGhostColor : enemyGhostColor;
|
||||
|
||||
foreach (MeshRenderer _renderer in GetComponentsInChildren<MeshRenderer>())
|
||||
_renderer.material.color = ghostColor;
|
||||
_renderer.material.color = _color;
|
||||
}
|
||||
|
||||
/// <summary> Вызывается на сервере сразу после NetworkObject.Spawn(). </summary>
|
||||
@@ -40,9 +66,12 @@ public class GhostPlayer : NetworkBehaviour
|
||||
shotIdx = 0;
|
||||
finished = false;
|
||||
|
||||
ownerId.Value = _recording.ownerClientId;
|
||||
|
||||
ownColliders = GetComponentsInChildren<Collider>();
|
||||
|
||||
health = GetComponent<PlayerHealth>();
|
||||
shield = GetComponent<PlayerShield>();
|
||||
// Init(true): мы на сервере. Без этого TakeDamage у клона молча не работает
|
||||
health.Init(true);
|
||||
}
|
||||
@@ -66,6 +95,9 @@ public class GhostPlayer : NetworkBehaviour
|
||||
transform.position = recording.positions[_tick];
|
||||
transform.rotation = recording.rotations[_tick];
|
||||
|
||||
if (shield != null && _tick < recording.shieldStates.Count)
|
||||
shield.SetShieldState(recording.shieldStates[_tick]);
|
||||
|
||||
while (shotIdx < recording.shots.Count && recording.shots[shotIdx].tick <= _tick)
|
||||
{
|
||||
ReplayShot(recording.shots[shotIdx]);
|
||||
@@ -80,7 +112,8 @@ public class GhostPlayer : NetworkBehaviour
|
||||
{
|
||||
if (System.Array.Exists(ownColliders, c => c == _hit.collider)) return;
|
||||
|
||||
if (_hit.collider.TryGetComponent(out PlayerHealth _targetHealth))
|
||||
PlayerHealth _targetHealth = _hit.collider.GetComponentInParent<PlayerHealth>();
|
||||
if (_targetHealth != null)
|
||||
_targetHealth.TakeDamage((int)damageAmount);
|
||||
|
||||
SpawnHitEffectClientRpc(_hit.point);
|
||||
|
||||
@@ -39,6 +39,9 @@ public class GhostRecorder : NetworkBehaviour
|
||||
Recording.positions.Add(transform.position);
|
||||
Recording.rotations.Add(transform.rotation);
|
||||
Recording.pitches.Add(cameraRoot != null ? cameraRoot.localEulerAngles.x : 0f);
|
||||
|
||||
PlayerShield _shield = GetComponent<PlayerShield>();
|
||||
Recording.shieldStates.Add(_shield != null && _shield.IsShieldUp);
|
||||
}
|
||||
|
||||
/// <summary> Дёргается из PlayerShoot.ShootServerRpc — только на сервере. </summary>
|
||||
|
||||
@@ -11,6 +11,7 @@ public class GhostRecording
|
||||
public List<Vector3> positions = new List<Vector3>(); // сэмпл каждый FixedUpdate
|
||||
public List<Quaternion> rotations = new List<Quaternion>(); // yaw тела
|
||||
public List<float> pitches = new List<float>(); // наклон камеры (на будущее, для визуала)
|
||||
public List<bool> shieldStates = new List<bool>(); // щит поднят — сэмпл каждый FixedUpdate
|
||||
|
||||
public List<GhostShotEvent> shots = new List<GhostShotEvent>();
|
||||
}
|
||||
|
||||
@@ -80,6 +80,13 @@ public class GhostRoundManager : MonoBehaviour
|
||||
roundTick++;
|
||||
}
|
||||
|
||||
/// <summary> Полностью очищает архив прошлых раундов (для рестарта матча). </summary>
|
||||
public void ClearArchive()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
archive.Clear();
|
||||
}
|
||||
|
||||
/// <summary> Вызывается из RoundManager.EndRound первой строкой (до WaitForSeconds). </summary>
|
||||
public void ServerEndRound()
|
||||
{
|
||||
|
||||
@@ -456,7 +456,8 @@ GameObject:
|
||||
- component: {fileID: 8786382977880960132}
|
||||
- component: {fileID: 6129752397730442859}
|
||||
- component: {fileID: 4822096153522273678}
|
||||
m_Layer: 0
|
||||
- component: {fileID: 9223372036854775807}
|
||||
m_Layer: 11
|
||||
m_Name: GhostPlayer
|
||||
m_TagString: Player
|
||||
m_Icon: {fileID: 0}
|
||||
@@ -596,6 +597,7 @@ MonoBehaviour:
|
||||
playerMovement: {fileID: 0}
|
||||
playerHealth: {fileID: 1055737657517928686}
|
||||
playerShoot: {fileID: 0}
|
||||
playerShield: {fileID: 9223372036854775807}
|
||||
playerUIController: {fileID: 0}
|
||||
--- !u!114 &6129752397730442859
|
||||
MonoBehaviour:
|
||||
@@ -610,10 +612,11 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
damageAmount: 10
|
||||
damageAmount: 30
|
||||
range: 100
|
||||
hitEffectPrefab: {fileID: 5162910912013668194, guid: cf82839c411151342a7b2337bda9fb34, type: 3}
|
||||
ghostColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||
ownGhostColor: {r: 0.3466981, g: 0.3466981, b: 0.5, a: 1}
|
||||
enemyGhostColor: {r: 0.5019608, g: 0.35940394, b: 0.35940394, a: 1}
|
||||
--- !u!114 &4822096153522273678
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -660,6 +663,26 @@ MonoBehaviour:
|
||||
SwitchTransformSpaceWhenParented: 0
|
||||
Interpolate: 1
|
||||
SlerpPosition: 0
|
||||
--- !u!114 &9223372036854775807
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2553183295301017937}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d749787957c32284a9de922295c60954, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
shieldVisual: {fileID: 2611096889943151719}
|
||||
shieldMoveSpeedMultiplier: 0.45
|
||||
isActive:
|
||||
UpdateTraits:
|
||||
MinSecondsBetweenUpdates: 0
|
||||
MaxSecondsBetweenUpdates: 0
|
||||
m_InternalValue: 0
|
||||
--- !u!1 &3046942642847312462
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1094,6 +1117,7 @@ Transform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 4744484706230885847}
|
||||
- {fileID: 3553217504045761826}
|
||||
m_Father: {fileID: 2264751121530880260}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!20 &358431225805014765
|
||||
@@ -1171,8 +1195,7 @@ MonoBehaviour:
|
||||
m_RequiresDepthTextureOption: 2
|
||||
m_RequiresOpaqueTextureOption: 2
|
||||
m_CameraType: 0
|
||||
m_Cameras:
|
||||
- {fileID: 0}
|
||||
m_Cameras: []
|
||||
m_RendererIndex: -1
|
||||
m_VolumeLayerMask:
|
||||
serializedVersion: 2
|
||||
@@ -1738,3 +1761,86 @@ MeshRenderer:
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!1001 &9044731326246825352
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 2142486437948056408}
|
||||
m_Modifications:
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 1.9440559
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 0.6993007
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 0.6993007
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: -0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: -1.12
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: 0.795
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: -0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: -0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: -0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6465249256851274223, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: vfx_Portal_01
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6465249256851274223, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_IsActive
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_SourcePrefab: {fileID: 100100000, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
--- !u!1 &2611096889943151719 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 6465249256851274223, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
m_PrefabInstance: {fileID: 9044731326246825352}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!4 &3553217504045761826 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
m_PrefabInstance: {fileID: 9044731326246825352}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
|
||||
@@ -30,12 +30,7 @@ Transform:
|
||||
m_LocalScale: {x: 1.4300001, y: 1.4300001, z: 1.4300001}
|
||||
m_ConstrainProportionsScale: 1
|
||||
m_Children:
|
||||
- {fileID: 6355811663250676540}
|
||||
- {fileID: 3631687216309567160}
|
||||
- {fileID: 2186247165251825076}
|
||||
- {fileID: 4622509540328270759}
|
||||
- {fileID: 837484978117059731}
|
||||
- {fileID: 6565998452162728716}
|
||||
- {fileID: 6275691048546651888}
|
||||
m_Father: {fileID: 2142486437948056408}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &8811223344556677889
|
||||
@@ -460,7 +455,8 @@ GameObject:
|
||||
- component: {fileID: 7845123456789012345}
|
||||
- component: {fileID: 3541666218759354771}
|
||||
- component: {fileID: -8178402565902839776}
|
||||
m_Layer: 0
|
||||
- component: {fileID: 9223372036854775807}
|
||||
m_Layer: 10
|
||||
m_Name: NetworkPlayer
|
||||
m_TagString: Player
|
||||
m_Icon: {fileID: 0}
|
||||
@@ -497,7 +493,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
GlobalObjectIdHash: 2748281708
|
||||
GlobalObjectIdHash: 389677881
|
||||
InScenePlacedSourceGlobalObjectIdHash: 2748281708
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
@@ -610,7 +606,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
damageAmount: 10
|
||||
damageAmount: 30
|
||||
range: 100
|
||||
cameraRoot: {fileID: 2142486437948056408}
|
||||
hitEffectPrefab: {fileID: 5162910912013668194, guid: cf82839c411151342a7b2337bda9fb34, type: 3}
|
||||
@@ -680,6 +676,7 @@ MonoBehaviour:
|
||||
playerMovement: {fileID: 7845123456789012345}
|
||||
playerHealth: {fileID: 1055737657517928686}
|
||||
playerShoot: {fileID: 3924378418118045030}
|
||||
playerShield: {fileID: 9223372036854775807}
|
||||
playerUIController: {fileID: 0}
|
||||
--- !u!114 &7845123456789012345
|
||||
MonoBehaviour:
|
||||
@@ -732,7 +729,7 @@ CharacterController:
|
||||
serializedVersion: 3
|
||||
m_Height: 2
|
||||
m_Radius: 0.5
|
||||
m_SlopeLimit: 50
|
||||
m_SlopeLimit: 65
|
||||
m_StepOffset: 0.4
|
||||
m_SkinWidth: 0.08
|
||||
m_MinMoveDistance: 0.001
|
||||
@@ -751,6 +748,26 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
cameraRoot: {fileID: 2142486437948056408}
|
||||
--- !u!114 &9223372036854775807
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2553183295301017937}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d749787957c32284a9de922295c60954, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
shieldVisual: {fileID: 8274206139305021325}
|
||||
shieldMoveSpeedMultiplier: 0.45
|
||||
isActive:
|
||||
UpdateTraits:
|
||||
MinSecondsBetweenUpdates: 0
|
||||
MaxSecondsBetweenUpdates: 0
|
||||
m_InternalValue: 0
|
||||
--- !u!1 &3046942642847312462
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -920,12 +937,12 @@ Transform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3262390447808528688}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.000000021855694, y: 0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: -0.000000044703484, y: 0.01862501, z: 0.041750014}
|
||||
m_LocalRotation: {x: 0.000000021855694, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0.018624993, z: 0.0417498}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4744484706230885847}
|
||||
m_Father: {fileID: 6275691048546651888}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &2164976477569351826
|
||||
MeshFilter:
|
||||
@@ -1006,12 +1023,12 @@ Transform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4263141307985599891}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.000000021855694, y: 0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: -0, y: -0.12762499, z: -0.04949999}
|
||||
m_LocalRotation: {x: 0.000000021855694, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: -0.12762497, z: -0.049500156}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4744484706230885847}
|
||||
m_Father: {fileID: 6275691048546651888}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &6527433930045468247
|
||||
MeshFilter:
|
||||
@@ -1184,6 +1201,7 @@ Transform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 7466977056233397960}
|
||||
- {fileID: 4744484706230885847}
|
||||
- {fileID: 3137815682427343959}
|
||||
m_Father: {fileID: 2264751121530880260}
|
||||
@@ -1426,12 +1444,12 @@ Transform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6002878583509321007}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.000000021855694, y: 0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: -0, y: -0.00012500212, z: 0.010500014}
|
||||
m_LocalRotation: {x: 0.000000021855694, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: -0.00012504471, z: 0.010499963}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4744484706230885847}
|
||||
m_Father: {fileID: 6275691048546651888}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &9193947969085399767
|
||||
MeshFilter:
|
||||
@@ -1486,6 +1504,43 @@ MeshRenderer:
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!1 &6826187805505678584
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 6275691048546651888}
|
||||
m_Layer: 6
|
||||
m_Name: Pistol
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &6275691048546651888
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6826187805505678584}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 6355811663250676540}
|
||||
- {fileID: 3631687216309567160}
|
||||
- {fileID: 2186247165251825076}
|
||||
- {fileID: 4622509540328270759}
|
||||
- {fileID: 837484978117059731}
|
||||
- {fileID: 6565998452162728716}
|
||||
m_Father: {fileID: 4744484706230885847}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &6864163356275009747
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1649,12 +1704,12 @@ Transform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7372121417174854672}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.000000021855694, y: 0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: -0, y: 0.03362499, z: 0.068}
|
||||
m_LocalRotation: {x: 0.000000021855694, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0.03362502, z: 0.068000525}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4744484706230885847}
|
||||
m_Father: {fileID: 6275691048546651888}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &7011163926604228964
|
||||
MeshFilter:
|
||||
@@ -1735,12 +1790,12 @@ Transform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7391264267075244707}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.000000021855694, y: 0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: -0, y: -0.121375024, z: -0.04949999}
|
||||
m_LocalRotation: {x: 0.000000021855694, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: -0.12137506, z: -0.049500156}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4744484706230885847}
|
||||
m_Father: {fileID: 6275691048546651888}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &7795375673714663475
|
||||
MeshFilter:
|
||||
@@ -1907,12 +1962,12 @@ Transform:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8160574590599177247}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.000000021855694, y: 0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: -0, y: 0.013624985, z: -0.082}
|
||||
m_LocalRotation: {x: 0.000000021855694, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0.013625039, z: -0.08199978}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4744484706230885847}
|
||||
m_Father: {fileID: 6275691048546651888}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &8016329216059766743
|
||||
MeshFilter:
|
||||
@@ -1967,3 +2022,86 @@ MeshRenderer:
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!1001 &3128540855029970530
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Modification:
|
||||
serializedVersion: 3
|
||||
m_TransformParent: {fileID: 2142486437948056408}
|
||||
m_Modifications:
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 2.78
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalPosition.y
|
||||
value: -1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalPosition.z
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalRotation.w
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalRotation.x
|
||||
value: -0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalRotation.y
|
||||
value: -0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalRotation.z
|
||||
value: -0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.y
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.z
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6465249256851274223, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_Name
|
||||
value: vfx_Portal_01
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6465249256851274223, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
propertyPath: m_IsActive
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
m_AddedGameObjects: []
|
||||
m_AddedComponents: []
|
||||
m_SourcePrefab: {fileID: 100100000, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
--- !u!4 &7466977056233397960 stripped
|
||||
Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 5533454199390677162, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
m_PrefabInstance: {fileID: 3128540855029970530}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &8274206139305021325 stripped
|
||||
GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 6465249256851274223, guid: d5774d459f0f43d4591f34994be1404e, type: 3}
|
||||
m_PrefabInstance: {fileID: 3128540855029970530}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
|
||||
@@ -10,6 +10,7 @@ public class PitZone : NetworkBehaviour
|
||||
private void OnTriggerEnter(Collider _other)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
if (RoundManager.Current == null || !RoundManager.Current.IsRoundActive) return;
|
||||
|
||||
// Клон — засчитываем вход за его ОРИГИНАЛЬНОГО игрока.
|
||||
@@ -19,6 +20,7 @@ public class PitZone : NetworkBehaviour
|
||||
if (_ghost != null)
|
||||
{
|
||||
RoundManager.Current.OnPlayerEnteredPit(pitIndex, _ghost.OriginalOwnerId);
|
||||
_ghost.GetComponent<PlayerHealth>().TakeDamage(999);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,5 +28,7 @@ public class PitZone : NetworkBehaviour
|
||||
if (_playerHealth == null) return;
|
||||
|
||||
RoundManager.Current.OnPlayerEnteredPit(pitIndex, _playerHealth.OwnerClientId);
|
||||
|
||||
_playerHealth.TakeDamage(999);
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,17 @@ public class PlayerFacade : NetworkBehaviour
|
||||
[SerializeField] private PlayerMovement playerMovement;
|
||||
[SerializeField] private PlayerHealth playerHealth;
|
||||
[SerializeField] private PlayerShoot playerShoot;
|
||||
[SerializeField] private PlayerShield playerShield;
|
||||
[SerializeField] private PlayerUIController playerUIController;
|
||||
|
||||
public PlayerHealth PlayerHealth => playerHealth;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// Клон (GhostPlayer) — серверный объект. На хосте он считается IsOwner,
|
||||
// из-за чего перехватывал HUD игрока. Игроковую логику на клоне не запускаем.
|
||||
if(GetComponent<GhostPlayer>() != null) return;
|
||||
|
||||
if(IsOwner)
|
||||
{
|
||||
StartCoroutine(InitPlayerUIController());
|
||||
@@ -27,6 +32,9 @@ public class PlayerFacade : NetworkBehaviour
|
||||
{
|
||||
playerShoot.Init();
|
||||
}
|
||||
|
||||
if (playerShield != null)
|
||||
playerShield.InitInput();
|
||||
}
|
||||
|
||||
if(playerHealth != null)
|
||||
|
||||
@@ -48,6 +48,9 @@ public class PlayerHealth : NetworkBehaviour
|
||||
{
|
||||
if (!isServer) return;
|
||||
if(isDead.Value) return;
|
||||
|
||||
PlayerShield _shield = GetComponent<PlayerShield>();
|
||||
if (_shield != null && _shield.IsShieldUp) return;
|
||||
|
||||
currentHealth.Value = Mathf.Max(0, currentHealth.Value - _damageAmount);
|
||||
}
|
||||
@@ -61,6 +64,7 @@ public class PlayerHealth : NetworkBehaviour
|
||||
{
|
||||
if(!isServer) return;
|
||||
|
||||
GetComponent<PlayerShield>()?.SetShieldState(false);
|
||||
isDead.Value = true;
|
||||
OnDiedClientRpc();
|
||||
}
|
||||
@@ -69,6 +73,7 @@ public class PlayerHealth : NetworkBehaviour
|
||||
{
|
||||
if(!isServer) return;
|
||||
|
||||
GetComponent<PlayerShield>()?.SetShieldState(false);
|
||||
isDead.Value = false;
|
||||
currentHealth.Value = maxHealth;
|
||||
RespawnClientRpc(_spawnPosition);
|
||||
|
||||
@@ -42,6 +42,7 @@ public class PlayerMovement : MonoBehaviour
|
||||
|
||||
private CharacterController controller;
|
||||
private PlayerHealth playerHealth;
|
||||
private PlayerShield playerShield;
|
||||
|
||||
private bool isActive;
|
||||
|
||||
@@ -60,6 +61,7 @@ public class PlayerMovement : MonoBehaviour
|
||||
{
|
||||
controller = GetComponent<CharacterController>();
|
||||
playerHealth = GetComponent<PlayerHealth>();
|
||||
playerShield = GetComponent<PlayerShield>();
|
||||
|
||||
// Встроенные возможности CharacterController — задаём из инспектора
|
||||
controller.stepOffset = stepOffset;
|
||||
@@ -136,8 +138,9 @@ public class PlayerMovement : MonoBehaviour
|
||||
input = Vector3.ClampMagnitude(input, 1f);
|
||||
|
||||
Vector3 wishDirection = transform.right * input.x + transform.forward * input.z;
|
||||
bool isSprinting = Input.GetKey(KeyCode.LeftShift);
|
||||
float targetSpeed = isSprinting ? sprintSpeed : walkSpeed;
|
||||
bool isSprinting = Input.GetKey(KeyCode.LeftShift) && (playerShield == null || !playerShield.IsShieldUp);
|
||||
float speedMult = playerShield != null ? playerShield.MoveSpeedMultiplier : 1f;
|
||||
float targetSpeed = (isSprinting ? sprintSpeed : walkSpeed) * speedMult;
|
||||
|
||||
bool grounded = controller.isGrounded;
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Щит: удержание F — поднят (нет стрельбы, иммунитет к пулям, замедление).
|
||||
/// Состояние синхронизируется через NetworkVariable (видно на клонах и у других игроков).
|
||||
/// </summary>
|
||||
public class PlayerShield : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private GameObject shieldVisual;
|
||||
[SerializeField] private float shieldMoveSpeedMultiplier = 0.45f;
|
||||
|
||||
public NetworkVariable<bool> isActive = new NetworkVariable<bool>(false);
|
||||
|
||||
private PlayerHealth playerHealth;
|
||||
private bool inputEnabled;
|
||||
|
||||
public bool IsShieldUp => isActive.Value;
|
||||
public float MoveSpeedMultiplier => isActive.Value ? shieldMoveSpeedMultiplier : 1f;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
isActive.OnValueChanged += OnShieldChanged;
|
||||
UpdateVisual(isActive.Value);
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
isActive.OnValueChanged -= OnShieldChanged;
|
||||
}
|
||||
|
||||
/// <summary> Владелец игрока — читает F и шлёт на сервер. </summary>
|
||||
public void InitInput()
|
||||
{
|
||||
inputEnabled = true;
|
||||
playerHealth = GetComponent<PlayerHealth>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!inputEnabled || !IsOwner) return;
|
||||
if (playerHealth != null && playerHealth.isDead.Value) return;
|
||||
|
||||
bool _wantShield = Input.GetKey(KeyCode.F);
|
||||
if (_wantShield == isActive.Value) return;
|
||||
|
||||
SetShieldServerRpc(_wantShield);
|
||||
}
|
||||
|
||||
/// <summary> Сервер: для клонов при воспроизведении записи. </summary>
|
||||
public void SetShieldState(bool _active)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
isActive.Value = _active;
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
private void SetShieldServerRpc(bool _active)
|
||||
{
|
||||
if (playerHealth == null) playerHealth = GetComponent<PlayerHealth>();
|
||||
if (playerHealth != null && playerHealth.isDead.Value) return;
|
||||
|
||||
isActive.Value = _active;
|
||||
}
|
||||
|
||||
private void OnShieldChanged(bool _previous, bool _current) => UpdateVisual(_current);
|
||||
|
||||
private void UpdateVisual(bool _active)
|
||||
{
|
||||
if (shieldVisual != null)
|
||||
shieldVisual.SetActive(_active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d749787957c32284a9de922295c60954
|
||||
@@ -11,6 +11,7 @@ public class PlayerShoot : NetworkBehaviour
|
||||
|
||||
private PlayerHealth playerHealth;
|
||||
private WeaponView weaponView;
|
||||
private PlayerShield playerShield;
|
||||
|
||||
private bool isActive = false;
|
||||
|
||||
@@ -19,6 +20,7 @@ public class PlayerShoot : NetworkBehaviour
|
||||
isActive = true;
|
||||
|
||||
playerHealth = GetComponent<PlayerHealth>();
|
||||
playerShield = GetComponent<PlayerShield>();
|
||||
weaponView = GetComponentInChildren<WeaponView>(true);
|
||||
}
|
||||
|
||||
@@ -27,6 +29,7 @@ public class PlayerShoot : NetworkBehaviour
|
||||
if (!isActive) return;
|
||||
|
||||
if (playerHealth == null || playerHealth.isDead.Value) return;
|
||||
if (playerShield != null && playerShield.IsShieldUp) return;
|
||||
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
@@ -50,7 +53,8 @@ public class PlayerShoot : NetworkBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (_hit.collider.TryGetComponent(out PlayerHealth _playerHealth))
|
||||
PlayerHealth _playerHealth = _hit.collider.GetComponentInParent<PlayerHealth>();
|
||||
if (_playerHealth != null)
|
||||
{
|
||||
_playerHealth.TakeDamage((int)damageAmount);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ public class RoundManager : NetworkBehaviour
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Рестарт матча по R — любой игрок, запрос уходит на сервер
|
||||
if (Input.GetKeyDown(KeyCode.R))
|
||||
RequestRestartServerRpc();
|
||||
|
||||
if (!IsServer) return;
|
||||
if (!IsRoundActive) return;
|
||||
|
||||
@@ -126,6 +130,41 @@ public class RoundManager : NetworkBehaviour
|
||||
// }
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestRestartServerRpc()
|
||||
{
|
||||
RestartMatch();
|
||||
}
|
||||
|
||||
// Полный рестарт: стоп текущего раунда и клонов, очистка архива и счёта, раунд с нуля
|
||||
private void RestartMatch()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
StopAllCoroutines();
|
||||
|
||||
if (GhostRoundManager.Current != null)
|
||||
{
|
||||
GhostRoundManager.Current.ServerEndRound();
|
||||
GhostRoundManager.Current.ClearArchive();
|
||||
}
|
||||
|
||||
for (int _i = 0; _i < playerScores.Count; _i++)
|
||||
{
|
||||
playerScores[_i] = new PlayerScore
|
||||
{
|
||||
ClientId = playerScores[_i].ClientId,
|
||||
Score = 0
|
||||
};
|
||||
}
|
||||
|
||||
isRoundActive.Value = false;
|
||||
roundTimeRemaining.Value = 0f;
|
||||
currentRound.Value = 0;
|
||||
|
||||
StartCoroutine(StartRoundWithDelay());
|
||||
}
|
||||
|
||||
public void RegisterPlayer(ulong _clientId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/// <summary>
|
||||
/// Хранит join-код текущей сессии между сценами (меню -> игра).
|
||||
/// Статик, поэтому переживает LoadScene, в отличие от UI сцены меню.
|
||||
/// </summary>
|
||||
public static class SessionInfo
|
||||
{
|
||||
public static string JoinCode { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7208b9716660ec48a0d5e40f4193347
|
||||
@@ -1,29 +1,143 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using Unity.Services.Multiplayer;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
public class MainMenuUIController : MonoBehaviour
|
||||
{
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private Button hostButton;
|
||||
[SerializeField] private Button clientButton;
|
||||
|
||||
[Header("Join by code")]
|
||||
[SerializeField] private TMP_InputField joinCodeInput;
|
||||
|
||||
[Header("Feedback")]
|
||||
[SerializeField] private TMP_Text joinCodeDisplay; // показывает код созданной сессии
|
||||
[SerializeField] private TMP_Text statusText; // статус/ошибки
|
||||
[SerializeField] private Button copyCodeButton; // копирование кода в буфер обмена
|
||||
|
||||
[Header("Session")]
|
||||
[SerializeField] private int maxPlayers = 2;
|
||||
[SerializeField] private string sessionType = "dev-game";
|
||||
[SerializeField] private string gameSceneName = "DEV-Game";
|
||||
|
||||
private ISession session;
|
||||
private bool isBusy;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
hostButton.onClick.AddListener(HostGame);
|
||||
clientButton.onClick.AddListener(JoinGame);
|
||||
hostButton.onClick.AddListener(OnHostClicked);
|
||||
clientButton.onClick.AddListener(OnJoinClicked);
|
||||
|
||||
if (copyCodeButton != null)
|
||||
{
|
||||
copyCodeButton.onClick.AddListener(CopyCode);
|
||||
copyCodeButton.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void HostGame()
|
||||
private void OnDestroy()
|
||||
{
|
||||
NetworkManager.Singleton.StartHost();
|
||||
NetworkManager.Singleton.SceneManager.LoadScene("DEV-Game", LoadSceneMode.Single);
|
||||
Debug.Log("Hosting game...");
|
||||
hostButton.onClick.RemoveListener(OnHostClicked);
|
||||
clientButton.onClick.RemoveListener(OnJoinClicked);
|
||||
|
||||
if (copyCodeButton != null)
|
||||
copyCodeButton.onClick.RemoveListener(CopyCode);
|
||||
}
|
||||
|
||||
private void JoinGame()
|
||||
private void CopyCode()
|
||||
{
|
||||
NetworkManager.Singleton.StartClient();
|
||||
Debug.Log("Joining game...");
|
||||
if (string.IsNullOrEmpty(SessionInfo.JoinCode)) return;
|
||||
|
||||
GUIUtility.systemCopyBuffer = SessionInfo.JoinCode;
|
||||
SetStatus($"Code copied: {SessionInfo.JoinCode}");
|
||||
}
|
||||
|
||||
private async void OnHostClicked()
|
||||
{
|
||||
if (isBusy) return;
|
||||
SetBusy(true);
|
||||
SetStatus("Creating session...");
|
||||
|
||||
try
|
||||
{
|
||||
SessionOptions _options = new SessionOptions
|
||||
{
|
||||
Name = $"session-{Guid.NewGuid().ToString().Substring(0, 6)}",
|
||||
MaxPlayers = maxPlayers,
|
||||
Type = sessionType
|
||||
};
|
||||
_options.WithRelayNetwork();
|
||||
|
||||
// Sessions API сам поднимает Relay и запускает NetworkManager как host
|
||||
session = await MultiplayerService.Instance.CreateSessionAsync(_options);
|
||||
|
||||
SessionInfo.JoinCode = session.Code;
|
||||
|
||||
if (joinCodeDisplay != null) joinCodeDisplay.text = session.Code;
|
||||
if (copyCodeButton != null) copyCodeButton.gameObject.SetActive(true);
|
||||
SetStatus($"Hosting. Code: {session.Code}");
|
||||
Debug.Log($"Session created. Join code: {session.Code}");
|
||||
|
||||
// Хост грузит игровую сцену — клиентам она синхронизируется автоматически.
|
||||
// Код сохранён в SessionInfo и показывается уже в игре (SessionCodeView).
|
||||
NetworkManager.Singleton.SceneManager.LoadScene(gameSceneName, LoadSceneMode.Single);
|
||||
}
|
||||
catch (Exception _e)
|
||||
{
|
||||
SetStatus($"Host failed: {_e.Message}");
|
||||
Debug.LogException(_e);
|
||||
SetBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnJoinClicked()
|
||||
{
|
||||
if (isBusy) return;
|
||||
|
||||
string _code = joinCodeInput != null ? joinCodeInput.text.Trim() : string.Empty;
|
||||
if (string.IsNullOrEmpty(_code))
|
||||
{
|
||||
SetStatus("Enter a join code first");
|
||||
return;
|
||||
}
|
||||
|
||||
SetBusy(true);
|
||||
SetStatus($"Joining {_code}...");
|
||||
|
||||
try
|
||||
{
|
||||
JoinSessionOptions _options = new JoinSessionOptions { Type = sessionType };
|
||||
|
||||
// Подключение к Relay и запуск NetworkManager как client — автоматически.
|
||||
// Игровая сцена придёт от хоста через сетевую синхронизацию сцен.
|
||||
session = await MultiplayerService.Instance.JoinSessionByCodeAsync(_code, _options);
|
||||
|
||||
SessionInfo.JoinCode = _code;
|
||||
SetStatus("Joined. Waiting for host scene...");
|
||||
Debug.Log($"Joined session {session.Id}");
|
||||
}
|
||||
catch (Exception _e)
|
||||
{
|
||||
SetStatus($"Join failed: {_e.Message}");
|
||||
Debug.LogException(_e);
|
||||
SetBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBusy(bool _busy)
|
||||
{
|
||||
isBusy = _busy;
|
||||
hostButton.interactable = !_busy;
|
||||
clientButton.interactable = !_busy;
|
||||
}
|
||||
|
||||
private void SetStatus(string _text)
|
||||
{
|
||||
if (statusText != null) statusText.text = _text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// Показывает join-код текущей сессии в игровой сцене и позволяет скопировать
|
||||
/// его в буфер обмена. Код берётся из SessionInfo (переживает загрузку сцены).
|
||||
/// Если кода нет (например, запуск через Play Mode без сессии) — прячет себя.
|
||||
/// </summary>
|
||||
public class SessionCodeView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_Text codeText;
|
||||
[SerializeField] private Button copyButton;
|
||||
[SerializeField] private GameObject root; // что скрывать, если кода нет (по умолчанию — этот объект)
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (root == null) root = gameObject;
|
||||
|
||||
if (copyButton != null)
|
||||
copyButton.onClick.AddListener(CopyCode);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (copyButton != null)
|
||||
copyButton.onClick.RemoveListener(CopyCode);
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
bool _hasCode = !string.IsNullOrEmpty(SessionInfo.JoinCode);
|
||||
root.SetActive(_hasCode);
|
||||
|
||||
if (_hasCode && codeText != null)
|
||||
codeText.text = SessionInfo.JoinCode;
|
||||
}
|
||||
|
||||
private void CopyCode()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SessionInfo.JoinCode)) return;
|
||||
|
||||
GUIUtility.systemCopyBuffer = SessionInfo.JoinCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbc186ba61c6e9740833330ee873b21a
|
||||
Reference in New Issue
Block a user