Added map and fixed other bugs

This commit is contained in:
2026-07-03 14:36:04 +03:00
parent 94739757be
commit 3d423974eb
161 changed files with 35735 additions and 315 deletions
@@ -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