This commit is contained in:
2026-08-21 17:59:13 +03:00
parent b98901c366
commit f6a7547e84
951 changed files with 909007 additions and 70938 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2b80031cb8a86414c9b054ed5a280465
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e069cd58c327cc94e9c2598c9e4c449f, type: 3}
m_Name: CannonConfig
m_EditorClassIdentifier:
minimumDelay: 0
maximumDelay: 30
defaultDelay: 5
adjustmentStep: 1
acceptDefectiveRockets: 1
acceptCriticallyDamagedRockets: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 23aa810e05d4cbc4da4bb729f5e54688
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 96e16f6f88696ea4c8b884f3e7cef9f7, type: 3}
m_Name: CannonShockwave
m_EditorClassIdentifier:
zones:
- Radius: 8
PlayerImpulse: 12
RigidbodyImpulse: 18
UpwardBias: 0.35
CameraShakeAmplitude: 0.22
CameraShakeDuration: 0.9
CameraShakeFrequency: 28
- Radius: 18
PlayerImpulse: 5
RigidbodyImpulse: 8
UpwardBias: 0.25
CameraShakeAmplitude: 0.12
CameraShakeDuration: 0.75
CameraShakeFrequency: 24
- Radius: 35
PlayerImpulse: 0
RigidbodyImpulse: 1.5
UpwardBias: 0.1
CameraShakeAmplitude: 0.055
CameraShakeDuration: 0.55
CameraShakeFrequency: 20
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9155e8ad462c3484ba4275bcb64babe2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 96e16f6f88696ea4c8b884f3e7cef9f7, type: 3}
m_Name: RocketImpactShockwave
m_EditorClassIdentifier:
zones:
- Radius: 4
PlayerImpulse: 8
RigidbodyImpulse: 12
UpwardBias: 0.4
CameraShakeAmplitude: 0.18
CameraShakeDuration: 0.7
CameraShakeFrequency: 27
- Radius: 8
PlayerImpulse: 3
RigidbodyImpulse: 5
UpwardBias: 0.3
CameraShakeAmplitude: 0.09
CameraShakeDuration: 0.55
CameraShakeFrequency: 23
- Radius: 14
PlayerImpulse: 0
RigidbodyImpulse: 0.5
UpwardBias: 0.1
CameraShakeAmplitude: 0.04
CameraShakeDuration: 0.4
CameraShakeFrequency: 18
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5afb0716a3dcecf469e0fe2128d5b98b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -16,4 +16,5 @@ MonoBehaviour:
displayName: Full-Rocket-Standart
worldPrefab: {fileID: 7456648717704822871, guid: 746f04d9ba6c87d4bae8a09287c5d090, type: 3}
rocketTypeId: standard
missileType: 1
criticalImpactSpeed: 7
@@ -17,6 +17,8 @@ GameObject:
- component: {fileID: 4269639932524941515}
- component: {fileID: 6493104616022895181}
- component: {fileID: 3635736001397410476}
- component: {fileID: 633656625536386338}
- component: {fileID: 4161244424843525666}
m_Layer: 0
m_Name: Ready-Rocket-Standart
m_TagString: Untagged
@@ -52,7 +54,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier:
GlobalObjectIdHash: 3332465624
GlobalObjectIdHash: 246746440
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
@@ -261,6 +263,44 @@ MonoBehaviour:
onCriticalImpactSpeed:
m_PersistentCalls:
m_Calls: []
--- !u!114 &633656625536386338
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 43f56ec799f9aec4ca1bf372c88592cd, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
profile: {fileID: 11400000, guid: 5afb0716a3dcecf469e0fe2128d5b98b, type: 2}
origin: {fileID: 1632953841127884940}
physicsLayers:
serializedVersion: 2
m_Bits: 4294967295
shockwaveEmitted:
m_PersistentCalls:
m_Calls: []
--- !u!114 &4161244424843525666
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ac11f143457835f46a1a2defaa624354, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
rocket: {fileID: 6493104616022895181}
shockwave: {fileID: 633656625536386338}
despawnAfterExplosion: 1
despawnDelay: 0.15
--- !u!1001 &5717474834163769460
PrefabInstance:
m_ObjectHideFlags: 0
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2dab561baa00bfb46b838b978cb45129
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using UnityEngine;
[CreateAssetMenu(
fileName = "CannonConfig",
menuName = "FORT-BO/Cannon/Cannon Config")]
public sealed class CannonConfig : ScriptableObject
{
[SerializeField, Min(0f)] private float minimumDelay = 0f;
[SerializeField, Min(0.1f)] private float maximumDelay = 60f;
[SerializeField, Min(0f)] private float defaultDelay = 5f;
[SerializeField, Min(0.1f)] private float adjustmentStep = 1f;
[SerializeField] private bool acceptDefectiveRockets = true;
[SerializeField] private bool acceptCriticallyDamagedRockets;
public float MinimumDelay => minimumDelay;
public float MaximumDelay => maximumDelay;
public float DefaultDelay => ClampDelay(defaultDelay);
public float AdjustmentStep => adjustmentStep;
public bool AcceptDefectiveRockets => acceptDefectiveRockets;
public bool AcceptCriticallyDamagedRockets => acceptCriticallyDamagedRockets;
public float ClampDelay(float value) =>
Mathf.Clamp(value, minimumDelay, Mathf.Max(minimumDelay, maximumDelay));
private void OnValidate()
{
minimumDelay = Mathf.Max(0f, minimumDelay);
maximumDelay = Mathf.Max(minimumDelay, maximumDelay);
defaultDelay = ClampDelay(defaultDelay);
adjustmentStep = Mathf.Max(0.1f, adjustmentStep);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e069cd58c327cc94e9c2598c9e4c449f
@@ -0,0 +1,51 @@
using UnityEngine;
[DisallowMultipleComponent]
public sealed class CannonControlBinder : MonoBehaviour
{
[SerializeField] private NetworkCannonController cannon;
[SerializeField] private NetworkPhysicalLeverInteractable fireLever;
[SerializeField] private NetworkButtonInteractable decreaseTimerButton;
[SerializeField] private NetworkButtonInteractable increaseTimerButton;
private bool bound;
private void Update()
{
if (!bound)
TryBind();
}
private void OnDisable() => Unbind();
private void TryBind()
{
if (cannon == null || !cannon.IsSpawned || !cannon.IsServer)
return;
if (fireLever != null)
fireLever.StateChanged += HandleFireLever;
if (decreaseTimerButton != null)
decreaseTimerButton.Pressed += HandleDecreaseTimer;
if (increaseTimerButton != null)
increaseTimerButton.Pressed += HandleIncreaseTimer;
bound = true;
}
private void Unbind()
{
if (!bound)
return;
if (fireLever != null)
fireLever.StateChanged -= HandleFireLever;
if (decreaseTimerButton != null)
decreaseTimerButton.Pressed -= HandleDecreaseTimer;
if (increaseTimerButton != null)
increaseTimerButton.Pressed -= HandleIncreaseTimer;
bound = false;
}
private void HandleFireLever(bool isActivated) => cannon.SetFireRequested(isActivated);
private void HandleDecreaseTimer() => cannon.RequestDecreaseShotDelay();
private void HandleIncreaseTimer() => cannon.RequestIncreaseShotDelay();
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 24a208cb322bf904bb12481f5fc58a85
@@ -0,0 +1,81 @@
using Unity.Netcode;
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(Collider))]
public sealed class CannonRocketSlot : MonoBehaviour, IInteractable
{
[SerializeField] private Transform snapPoint;
private NetworkCannonController cannon;
private Collider triggerCollider;
public Transform SnapPoint => snapPoint != null ? snapPoint : transform;
public Vector3 InteractionPosition => SnapPoint.position;
public string InteractionPrompt => cannon != null
? cannon.LoadedRocket != null
? $"Take {cannon.LoadedRocket.RocketTypeId} rocket"
: "Rocket slot"
: "Rocket slot";
public bool IsInteractionAvailable => cannon != null && cannon.CanTakeRocket;
private void Reset()
{
snapPoint = transform;
ResolveCollider();
triggerCollider.isTrigger = true;
}
private void Awake() => ResolveCollider();
private void OnValidate()
{
ResolveCollider();
if (triggerCollider != null)
triggerCollider.isTrigger = true;
}
public void Configure(NetworkCannonController owner) => cannon = owner;
public void RequestInteraction()
{
if (IsInteractionAvailable)
cannon.RequestTakeRocket();
}
private void OnTriggerEnter(Collider other) => TryInsert(other);
private void OnTriggerStay(Collider other) => TryInsert(other);
private void TryInsert(Collider other)
{
if (cannon == null || !cannon.IsServer || !cannon.IsSpawned)
return;
NetworkRocketItem rocket = other.GetComponentInParent<NetworkRocketItem>();
if (rocket != null)
cannon.TryInsertRocket(rocket);
}
public bool OverlapsAny(Collider[] itemColliders, float clearance)
{
ResolveCollider();
if (triggerCollider == null || itemColliders == null)
return false;
Bounds bounds = triggerCollider.bounds;
bounds.Expand(Mathf.Max(0f, clearance) * 2f);
foreach (Collider itemCollider in itemColliders)
{
if (itemCollider != null && itemCollider.enabled &&
bounds.Intersects(itemCollider.bounds))
return true;
}
return false;
}
private void ResolveCollider()
{
if (triggerCollider == null)
triggerCollider = GetComponent<Collider>();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7ad0f04c740f9b343b2ed4f083c407a6
@@ -0,0 +1,46 @@
using TMPro;
using UnityEngine;
[DisallowMultipleComponent]
public sealed class CannonTimerView : MonoBehaviour
{
[SerializeField] private NetworkCannonController controller;
[SerializeField] private TMP_Text timerText;
[SerializeField] private string emptyText = "INSERT ROCKET";
[SerializeField] private string loadedFormat = "DELAY {0:0}s";
[SerializeField] private string countdownFormat = "FIRE IN {0:0.0}s";
private void OnEnable()
{
if (controller != null)
controller.Changed += Refresh;
Refresh();
}
private void OnDisable()
{
if (controller != null)
controller.Changed -= Refresh;
}
private void Update()
{
if (controller != null && controller.State == CannonState.Countdown)
Refresh();
}
private void Refresh()
{
if (timerText == null || controller == null)
return;
timerText.text = controller.State switch
{
CannonState.Empty => emptyText,
CannonState.Loaded => string.Format(loadedFormat, controller.SelectedDelay),
CannonState.Countdown => string.Format(
countdownFormat, controller.RemainingSeconds),
_ => emptyText
};
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 451ab5135cfc56f489740516a23e7834
@@ -0,0 +1,90 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public sealed class CannonView : MonoBehaviour
{
[SerializeField] private NetworkCannonController controller;
[SerializeField] private Transform recoilVisual;
[SerializeField] private Vector3 localRecoilOffset = new(0f, 0f, -0.75f);
[SerializeField, Min(0.01f)] private float recoilOutDuration = 0.08f;
[SerializeField, Min(0.01f)] private float recoilReturnDuration = 0.45f;
[SerializeField] private UnityEvent shotFired;
[SerializeField] private UnityEvent<MissileType> missileFired;
private Vector3 baseLocalPosition;
private Coroutine recoilRoutine;
private void Awake()
{
if (recoilVisual != null)
baseLocalPosition = recoilVisual.localPosition;
}
private void OnEnable()
{
if (controller != null)
controller.ShotFired += HandleShotFired;
}
private void OnDisable()
{
if (controller != null)
controller.ShotFired -= HandleShotFired;
if (recoilRoutine != null)
StopCoroutine(recoilRoutine);
if (recoilVisual != null)
recoilVisual.localPosition = baseLocalPosition;
}
private void HandleShotFired(CannonShotState shot)
{
if (recoilRoutine != null)
StopCoroutine(recoilRoutine);
recoilRoutine = StartCoroutine(PlayRecoil());
shotFired?.Invoke();
missileFired?.Invoke(shot.MissileType);
}
private IEnumerator PlayRecoil()
{
if (recoilVisual == null)
yield break;
Vector3 recoilPosition = baseLocalPosition + localRecoilOffset;
yield return Move(recoilVisual.localPosition, recoilPosition, recoilOutDuration);
yield return Move(recoilVisual.localPosition, baseLocalPosition, recoilReturnDuration);
recoilRoutine = null;
}
private IEnumerator Move(Vector3 from, Vector3 to, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
recoilVisual.localPosition = Vector3.LerpUnclamped(
from, to, 1f - Mathf.Pow(1f - t, 3f));
yield return null;
}
recoilVisual.localPosition = to;
}
[ContextMenu("Preview/Recoil")]
private void PreviewRecoil()
{
if (Application.isPlaying)
HandleShotFired(new CannonShotState(0, MissileType.Standard));
else if (recoilVisual != null)
recoilVisual.localPosition = baseLocalPosition + localRecoilOffset;
}
[ContextMenu("Preview/Reset")]
private void ResetPreview()
{
if (recoilVisual != null)
recoilVisual.localPosition = baseLocalPosition;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f174c161f1871f44bba1b7e09ce03e61
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4c3f0760c12468543bab2cf3f99294f6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,458 @@
using TMPro;
using Unity.Netcode;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
public static class CannonPrototypeSetup
{
private const string DataFolder = "Assets/CONTENT/Game/Data/Cannon";
private const string CannonConfigPath = DataFolder + "/CannonConfig.asset";
private const string CannonShockwavePath = DataFolder + "/CannonShockwave.asset";
private const string RocketShockwavePath = DataFolder + "/RocketImpactShockwave.asset";
private const string PlayerPrefabPath =
"Assets/CONTENT/Friendslop/Prefabs/NetworkPlayer.prefab";
private const string RocketPrefabPath =
"Assets/CONTENT/Game/Prefabs/Items/Ready-Rocket-Standart.prefab";
private const string StandardRocketDefinitionPath =
"Assets/CONTENT/Game/Data/Items/Rocket-Ready/Ready-Rocket-Standart.asset";
private const string LeverPrefabPath =
"Assets/CONTENT/Game/Prefabs/Interaction/PhysicalLever.prefab";
[MenuItem("FORT-BO/Cannon/Configure Network Prototype")]
public static void ConfigureNetworkPrototype()
{
if (Application.isPlaying)
{
Debug.LogWarning("Stop Play Mode before configuring the cannon prototype.");
return;
}
EnsureDataFolder();
CannonConfig config = GetOrCreateCannonConfig();
ShockwaveProfile cannonShockwave = GetOrCreateProfile(
CannonShockwavePath,
new ShockwaveZone
{
Radius = 8f,
PlayerImpulse = 12f,
RigidbodyImpulse = 18f,
UpwardBias = 0.35f,
CameraShakeAmplitude = 0.22f,
CameraShakeDuration = 0.9f,
CameraShakeFrequency = 28f
},
new ShockwaveZone
{
Radius = 18f,
PlayerImpulse = 5f,
RigidbodyImpulse = 8f,
UpwardBias = 0.25f,
CameraShakeAmplitude = 0.12f,
CameraShakeDuration = 0.75f,
CameraShakeFrequency = 24f
},
new ShockwaveZone
{
Radius = 35f,
PlayerImpulse = 0f,
RigidbodyImpulse = 1.5f,
UpwardBias = 0.1f,
CameraShakeAmplitude = 0.055f,
CameraShakeDuration = 0.55f,
CameraShakeFrequency = 20f
});
ShockwaveProfile rocketShockwave = GetOrCreateProfile(
RocketShockwavePath,
new ShockwaveZone
{
Radius = 4f,
PlayerImpulse = 8f,
RigidbodyImpulse = 12f,
UpwardBias = 0.4f,
CameraShakeAmplitude = 0.18f,
CameraShakeDuration = 0.7f,
CameraShakeFrequency = 27f
},
new ShockwaveZone
{
Radius = 8f,
PlayerImpulse = 3f,
RigidbodyImpulse = 5f,
UpwardBias = 0.3f,
CameraShakeAmplitude = 0.09f,
CameraShakeDuration = 0.55f,
CameraShakeFrequency = 23f
},
new ShockwaveZone
{
Radius = 14f,
PlayerImpulse = 0f,
RigidbodyImpulse = 0.5f,
UpwardBias = 0.1f,
CameraShakeAmplitude = 0.04f,
CameraShakeDuration = 0.4f,
CameraShakeFrequency = 18f
});
ConfigurePlayerPrefab();
ConfigureRocketPrefab(rocketShockwave);
ConfigureStandardRocketDefinition();
NetworkCannonController cannon = ConfigureCannonScene(config, cannonShockwave);
ConfigureMapMarker();
AssetDatabase.SaveAssets();
EditorSceneManager.MarkSceneDirty(cannon.gameObject.scene);
EditorSceneManager.SaveOpenScenes();
Selection.activeObject = cannon.gameObject;
Debug.Log(
"[Cannon] Network prototype configured. Insert a ready rocket, " +
"set delay with -/+ buttons and pull the fire lever.", cannon);
}
private static CannonConfig GetOrCreateCannonConfig()
{
CannonConfig config = AssetDatabase.LoadAssetAtPath<CannonConfig>(CannonConfigPath);
if (config == null)
{
config = ScriptableObject.CreateInstance<CannonConfig>();
AssetDatabase.CreateAsset(config, CannonConfigPath);
}
SerializedObject serialized = new(config);
SetFloat(serialized, "minimumDelay", 0f);
SetFloat(serialized, "maximumDelay", 30f);
SetFloat(serialized, "defaultDelay", 5f);
SetFloat(serialized, "adjustmentStep", 1f);
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(config);
return config;
}
private static ShockwaveProfile GetOrCreateProfile(
string path,
params ShockwaveZone[] zones)
{
ShockwaveProfile profile = AssetDatabase.LoadAssetAtPath<ShockwaveProfile>(path);
if (profile == null)
{
profile = ScriptableObject.CreateInstance<ShockwaveProfile>();
AssetDatabase.CreateAsset(profile, path);
}
SerializedObject serialized = new(profile);
SerializedProperty array = serialized.FindProperty("zones");
array.arraySize = zones.Length;
for (int index = 0; index < zones.Length; index++)
{
SerializedProperty element = array.GetArrayElementAtIndex(index);
ShockwaveZone zone = zones[index];
element.FindPropertyRelative("Radius").floatValue = zone.Radius;
element.FindPropertyRelative("PlayerImpulse").floatValue = zone.PlayerImpulse;
element.FindPropertyRelative("RigidbodyImpulse").floatValue = zone.RigidbodyImpulse;
element.FindPropertyRelative("UpwardBias").floatValue = zone.UpwardBias;
element.FindPropertyRelative("CameraShakeAmplitude").floatValue =
zone.CameraShakeAmplitude;
element.FindPropertyRelative("CameraShakeDuration").floatValue =
zone.CameraShakeDuration;
element.FindPropertyRelative("CameraShakeFrequency").floatValue =
zone.CameraShakeFrequency;
}
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(profile);
return profile;
}
private static void ConfigurePlayerPrefab()
{
GameObject root = PrefabUtility.LoadPrefabContents(PlayerPrefabPath);
try
{
GetOrAdd<PlayerCameraShakeController>(root);
GetOrAdd<PlayerImpactController>(root);
PrefabUtility.SaveAsPrefabAsset(root, PlayerPrefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
private static void ConfigureRocketPrefab(ShockwaveProfile rocketShockwave)
{
GameObject root = PrefabUtility.LoadPrefabContents(RocketPrefabPath);
try
{
NetworkRocketItem rocket = root.GetComponent<NetworkRocketItem>();
NetworkShockwaveEmitter emitter = GetOrAdd<NetworkShockwaveEmitter>(root);
RocketImpactExplosion explosion = GetOrAdd<RocketImpactExplosion>(root);
SerializedObject serializedEmitter = new(emitter);
SetObject(serializedEmitter, "profile", rocketShockwave);
SetObject(serializedEmitter, "origin", root.transform);
serializedEmitter.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedExplosion = new(explosion);
SetObject(serializedExplosion, "rocket", rocket);
SetObject(serializedExplosion, "shockwave", emitter);
serializedExplosion.ApplyModifiedPropertiesWithoutUndo();
PrefabUtility.SaveAsPrefabAsset(root, RocketPrefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
private static void ConfigureStandardRocketDefinition()
{
RocketItemDefinition definition =
AssetDatabase.LoadAssetAtPath<RocketItemDefinition>(StandardRocketDefinitionPath);
if (definition == null)
return;
SerializedObject serialized = new(definition);
serialized.FindProperty("missileType").enumValueIndex = (int)MissileType.Standard;
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(definition);
}
private static NetworkCannonController ConfigureCannonScene(
CannonConfig config,
ShockwaveProfile cannonShockwave)
{
GameObject root = GameObject.Find("Cannon Prototype");
bool created = root == null;
if (created)
{
root = new GameObject("Cannon Prototype");
root.transform.SetPositionAndRotation(
new Vector3(132f, 2.55f, 24.3f),
Quaternion.Euler(0f, 90f, 0f));
Undo.RegisterCreatedObjectUndo(root, "Create cannon prototype");
}
GetOrAdd<NetworkObject>(root);
NetworkCannonController controller = GetOrAdd<NetworkCannonController>(root);
NetworkShockwaveEmitter emitter = GetOrAdd<NetworkShockwaveEmitter>(root);
CannonControlBinder binder = GetOrAdd<CannonControlBinder>(root);
CannonView view = GetOrAdd<CannonView>(root);
Transform baseVisual = GetOrCreatePrimitive(
root.transform, "Cannon Base", PrimitiveType.Cube);
baseVisual.localPosition = new Vector3(0f, 0.75f, 0f);
baseVisual.localRotation = Quaternion.identity;
baseVisual.localScale = new Vector3(2.2f, 1.5f, 2.2f);
Transform recoil = GetOrCreateChild(root.transform, "Barrel Recoil");
recoil.localPosition = new Vector3(0f, 2.1f, 0f);
recoil.localRotation = Quaternion.identity;
recoil.localScale = Vector3.one;
Transform barrel = GetOrCreatePrimitive(recoil, "Barrel", PrimitiveType.Cube);
barrel.localPosition = new Vector3(0f, 0f, 2.1f);
barrel.localRotation = Quaternion.identity;
barrel.localScale = new Vector3(0.8f, 0.8f, 4.2f);
Transform muzzle = GetOrCreateChild(recoil, "Muzzle");
muzzle.localPosition = new Vector3(0f, 0f, 4.25f);
Transform slotObject = GetOrCreateChild(root.transform, "Rocket Slot");
slotObject.localPosition = new Vector3(-2.2f, 1.25f, 0f);
slotObject.localRotation = Quaternion.Euler(0f, 90f, 0f);
slotObject.localScale = Vector3.one;
BoxCollider slotCollider = GetOrAdd<BoxCollider>(slotObject.gameObject);
slotCollider.isTrigger = true;
slotCollider.size = new Vector3(1.4f, 1.4f, 3.2f);
CannonRocketSlot slot = GetOrAdd<CannonRocketSlot>(slotObject.gameObject);
Transform snapPoint = GetOrCreateChild(slotObject, "Rocket Snap Point");
snapPoint.localPosition = Vector3.zero;
snapPoint.localRotation = Quaternion.identity;
Transform controls = GetOrCreateChild(root.transform, "Controls");
controls.localPosition = Vector3.zero;
controls.localRotation = Quaternion.identity;
controls.localScale = Vector3.one;
NetworkPhysicalLeverInteractable lever = CreateLever(controls);
NetworkButtonInteractable minus = CreateButton(
controls, "Timer Minus", new Vector3(1.8f, 1.4f, -1.4f), "Timer -");
NetworkButtonInteractable plus = CreateButton(
controls, "Timer Plus", new Vector3(3.1f, 1.4f, -1.4f), "Timer +");
TMP_Text timerText = CreateTimerText(controls);
SerializedObject serializedEmitter = new(emitter);
SetObject(serializedEmitter, "profile", cannonShockwave);
SetObject(serializedEmitter, "origin", muzzle);
serializedEmitter.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedController = new(controller);
SetObject(serializedController, "config", config);
SetObject(serializedController, "rocketSlot", slot);
SetObject(serializedController, "rocketSnapPoint", snapPoint);
SetObject(serializedController, "mapController",
Object.FindFirstObjectByType<MapController>(FindObjectsInactive.Include));
SetObject(serializedController, "shotShockwave", emitter);
serializedController.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedSlot = new(slot);
SetObject(serializedSlot, "snapPoint", snapPoint);
serializedSlot.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedBinder = new(binder);
SetObject(serializedBinder, "cannon", controller);
SetObject(serializedBinder, "fireLever", lever);
SetObject(serializedBinder, "decreaseTimerButton", minus);
SetObject(serializedBinder, "increaseTimerButton", plus);
serializedBinder.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedView = new(view);
SetObject(serializedView, "controller", controller);
SetObject(serializedView, "recoilVisual", recoil);
serializedView.ApplyModifiedPropertiesWithoutUndo();
CannonTimerView timerView = GetOrAdd<CannonTimerView>(timerText.gameObject);
SerializedObject serializedTimer = new(timerView);
SetObject(serializedTimer, "controller", controller);
SetObject(serializedTimer, "timerText", timerText);
serializedTimer.ApplyModifiedPropertiesWithoutUndo();
slot.Configure(controller);
EditorUtility.SetDirty(root);
return controller;
}
private static NetworkPhysicalLeverInteractable CreateLever(Transform parent)
{
Transform existing = parent.Find("Fire Lever");
GameObject root;
if (existing != null)
{
root = existing.gameObject;
}
else
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(LeverPrefabPath);
root = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
root.name = "Fire Lever";
}
root.transform.localPosition = new Vector3(4.5f, 1.25f, -1.2f);
root.transform.localRotation = Quaternion.Euler(0f, -90f, 0f);
root.transform.localScale = Vector3.one * 0.65f;
NetworkPhysicalLeverInteractable lever =
root.GetComponent<NetworkPhysicalLeverInteractable>();
SerializedObject serialized = new(lever);
SetString(serialized, "interactionLabel", "Fire cannon");
SetBool(serialized, "initialPositionIsMaximum", false);
SetBool(serialized, "returnToInitialPositionAfterSwitch", true);
SetFloat(serialized, "automaticReturnDelay", 0.2f);
serialized.ApplyModifiedPropertiesWithoutUndo();
return lever;
}
private static NetworkButtonInteractable CreateButton(
Transform parent,
string name,
Vector3 localPosition,
string label)
{
Transform root = GetOrCreateChild(parent, name);
root.localPosition = localPosition;
root.localRotation = Quaternion.identity;
root.localScale = Vector3.one;
GetOrAdd<NetworkObject>(root.gameObject);
Transform visual = GetOrCreatePrimitive(root, "Button Visual", PrimitiveType.Cube);
visual.localPosition = Vector3.zero;
visual.localRotation = Quaternion.identity;
visual.localScale = new Vector3(0.8f, 0.8f, 0.3f);
NetworkButtonInteractable button = GetOrAdd<NetworkButtonInteractable>(root.gameObject);
SerializedObject serialized = new(button);
SetString(serialized, "interactionLabel", label);
SetFloat(serialized, "maximumInteractionDistance", 3.5f);
serialized.ApplyModifiedPropertiesWithoutUndo();
return button;
}
private static TMP_Text CreateTimerText(Transform parent)
{
Transform root = GetOrCreateChild(parent, "Timer Display");
root.localPosition = new Vector3(2.45f, 2.25f, -1.35f);
root.localRotation = Quaternion.Euler(0f, 180f, 0f);
root.localScale = Vector3.one * 0.12f;
TextMeshPro text = GetOrAdd<TextMeshPro>(root.gameObject);
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 4f;
text.text = "INSERT ROCKET";
text.rectTransform.sizeDelta = new Vector2(12f, 3f);
return text;
}
private static void ConfigureMapMarker()
{
MapController map = Object.FindFirstObjectByType<MapController>(FindObjectsInactive.Include);
if (map == null)
return;
Transform root = map.transform.Find("Impact Markers");
if (root == null)
{
root = new GameObject("Impact Markers").transform;
root.SetParent(map.transform, false);
}
MapImpactMarkerView view = GetOrAdd<MapImpactMarkerView>(root.gameObject);
view.Configure(map.CoordinateSystem);
SerializedObject serializedMap = new(map);
SetObject(serializedMap, "impactMarkerView", view);
serializedMap.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(map);
}
private static void EnsureDataFolder()
{
if (!AssetDatabase.IsValidFolder("Assets/CONTENT/Game/Data"))
AssetDatabase.CreateFolder("Assets/CONTENT/Game", "Data");
if (!AssetDatabase.IsValidFolder(DataFolder))
AssetDatabase.CreateFolder("Assets/CONTENT/Game/Data", "Cannon");
}
private static Transform GetOrCreateChild(Transform parent, string name)
{
Transform existing = parent.Find(name);
if (existing != null)
return existing;
GameObject child = new(name);
child.transform.SetParent(parent, false);
Undo.RegisterCreatedObjectUndo(child, $"Create {name}");
return child.transform;
}
private static Transform GetOrCreatePrimitive(
Transform parent,
string name,
PrimitiveType primitiveType)
{
Transform existing = parent.Find(name);
if (existing != null)
return existing;
GameObject primitive = GameObject.CreatePrimitive(primitiveType);
primitive.name = name;
primitive.transform.SetParent(parent, false);
Undo.RegisterCreatedObjectUndo(primitive, $"Create {name}");
return primitive.transform;
}
private static T GetOrAdd<T>(GameObject target) where T : Component
{
T component = target.GetComponent<T>();
return component != null ? component : Undo.AddComponent<T>(target);
}
private static void SetObject(SerializedObject target, string property, Object value) =>
target.FindProperty(property).objectReferenceValue = value;
private static void SetFloat(SerializedObject target, string property, float value) =>
target.FindProperty(property).floatValue = value;
private static void SetBool(SerializedObject target, string property, bool value) =>
target.FindProperty(property).boolValue = value;
private static void SetString(SerializedObject target, string property, string value) =>
target.FindProperty(property).stringValue = value;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fe3002dfbec1fe1458dcd6058b1486c3
@@ -0,0 +1,388 @@
using System;
using Unity.Netcode;
using UnityEngine;
public enum CannonState : byte
{
Empty,
Loaded,
Countdown
}
public struct CannonShotState : INetworkSerializable, IEquatable<CannonShotState>
{
public uint Sequence;
public MissileType MissileType;
public CannonShotState(uint sequence, MissileType missileType)
{
Sequence = sequence;
MissileType = missileType;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
where T : IReaderWriter
{
serializer.SerializeValue(ref Sequence);
serializer.SerializeValue(ref MissileType);
}
public bool Equals(CannonShotState other) =>
Sequence == other.Sequence && MissileType == other.MissileType;
}
[DisallowMultipleComponent]
[RequireComponent(typeof(NetworkObject))]
public sealed class NetworkCannonController : NetworkBehaviour
{
private const ulong EmptyRocketId = ulong.MaxValue;
[Header("Configuration")]
[SerializeField] private CannonConfig config;
[Header("Rocket Slot")]
[SerializeField] private CannonRocketSlot rocketSlot;
[SerializeField] private Transform rocketSnapPoint;
[SerializeField, Min(0.5f)] private float maximumTakeDistance = 4f;
[SerializeField, Min(0f)] private float minimumReinsertBlockDuration = 0.1f;
[Header("Output")]
[SerializeField] private MapController mapController;
[SerializeField] private NetworkShockwaveEmitter shotShockwave;
private readonly NetworkVariable<CannonState> state = new(
CannonState.Empty,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<ulong> loadedRocketId = new(
EmptyRocketId,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<float> selectedDelay = new(
5f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> fireAt = new(
0d,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<CannonShotState> lastShot = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private NetworkRocketItem loadedRocket;
private IWorkstationItemControl loadedRocketControl;
private ulong blockedRocketId = EmptyRocketId;
private Collider[] blockedRocketColliders;
private double unblockAfter;
public CannonState State => state.Value;
public CannonConfig Config => config;
public NetworkRocketItem LoadedRocket => loadedRocket != null
? loadedRocket
: ResolveLoadedRocket();
public float SelectedDelay => selectedDelay.Value;
public double FireAt => fireAt.Value;
public float RemainingSeconds => State == CannonState.Countdown && NetworkManager != null
? Mathf.Max(0f, (float)(fireAt.Value - NetworkManager.ServerTime.Time))
: 0f;
public bool CanTakeRocket => IsSpawned && State == CannonState.Loaded &&
loadedRocketId.Value != EmptyRocketId;
public CannonShotState LastShot => lastShot.Value;
public event Action Changed;
public event Action<CannonShotState> ShotFired;
private void Awake()
{
if (rocketSnapPoint == null && rocketSlot != null)
rocketSnapPoint = rocketSlot.SnapPoint;
rocketSlot?.Configure(this);
}
public override void OnNetworkSpawn()
{
state.OnValueChanged += HandleStateChanged;
loadedRocketId.OnValueChanged += HandleRocketIdChanged;
selectedDelay.OnValueChanged += HandleDelayChanged;
fireAt.OnValueChanged += HandleFireAtChanged;
lastShot.OnValueChanged += HandleShotChanged;
rocketSlot?.Configure(this);
if (IsServer)
{
state.Value = CannonState.Empty;
loadedRocketId.Value = EmptyRocketId;
selectedDelay.Value = config != null ? config.DefaultDelay : 5f;
fireAt.Value = 0d;
}
Changed?.Invoke();
}
public override void OnNetworkDespawn()
{
state.OnValueChanged -= HandleStateChanged;
loadedRocketId.OnValueChanged -= HandleRocketIdChanged;
selectedDelay.OnValueChanged -= HandleDelayChanged;
fireAt.OnValueChanged -= HandleFireAtChanged;
lastShot.OnValueChanged -= HandleShotChanged;
}
private void Update()
{
if (!IsServer || !IsSpawned)
return;
UpdateReinsertBlock();
if (State == CannonState.Countdown &&
NetworkManager != null && NetworkManager.ServerTime.Time >= fireAt.Value)
{
FireOnServer();
}
}
private void LateUpdate()
{
if (!IsServer || !IsSpawned || LoadedRocket == null ||
rocketSnapPoint == null)
return;
loadedRocket.transform.SetPositionAndRotation(
rocketSnapPoint.position,
rocketSnapPoint.rotation);
}
public bool TryInsertRocket(NetworkRocketItem rocket)
{
if (!IsServer || !IsSpawned || State != CannonState.Empty ||
rocket == null || !rocket.IsSpawned || rocket.Definition == null ||
rocket.NetworkObjectId == blockedRocketId ||
(config != null && !config.AcceptDefectiveRockets && rocket.IsDefective) ||
(config != null && !config.AcceptCriticallyDamagedRockets &&
rocket.IsCriticallyDamaged) ||
!WorkstationItemControlUtility.TryGetControl(
rocket.GetComponent<NetworkItem>(),
out IWorkstationItemControl control) ||
!control.TryAcquireExternalControl())
{
return false;
}
loadedRocket = rocket;
loadedRocketControl = control;
loadedRocketId.Value = rocket.NetworkObjectId;
state.Value = CannonState.Loaded;
SnapLoadedRocket();
return true;
}
public void RequestTakeRocket()
{
if (CanTakeRocket)
RequestTakeRocketRpc();
}
public void RequestSetShotDelay(float seconds)
{
if (!IsSpawned)
return;
if (IsServer)
SetShotDelayOnServer(seconds);
else
RequestSetShotDelayRpc(seconds);
}
public void RequestAdjustShotDelay(float delta)
{
RequestSetShotDelay(SelectedDelay + delta);
}
public void RequestIncreaseShotDelay()
{
RequestAdjustShotDelay(config != null ? config.AdjustmentStep : 1f);
}
public void RequestDecreaseShotDelay()
{
RequestAdjustShotDelay(-(config != null ? config.AdjustmentStep : 1f));
}
public void RequestStartCountdown()
{
if (!IsSpawned)
return;
if (IsServer)
TryStartCountdownOnServer();
else
RequestStartCountdownRpc();
}
public void SetFireRequested(bool isActivated)
{
if (isActivated && IsServer && IsSpawned)
TryStartCountdownOnServer();
}
[ContextMenu("Debug/Start Countdown (Server Play Mode)")]
private void StartCountdownFromContextMenu()
{
if (Application.isPlaying)
TryStartCountdownOnServer();
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestSetShotDelayRpc(float seconds) => SetShotDelayOnServer(seconds);
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestStartCountdownRpc() => TryStartCountdownOnServer();
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestTakeRocketRpc(RpcParams rpcParams = default)
{
if (!CanTakeRocket || rocketSlot == null ||
!TryGetPlayerObject(rpcParams.Receive.SenderClientId, out NetworkObject player))
return;
float maxDistanceSquared = maximumTakeDistance * maximumTakeDistance;
if ((player.transform.position - rocketSlot.InteractionPosition).sqrMagnitude >
maxDistanceSquared)
return;
NetworkRocketItem rocket = LoadedRocket;
IWorkstationItemControl control = loadedRocketControl ?? ResolveLoadedControl();
if (rocket == null || control == null ||
!control.TryReleaseExternalControlToPlayer(rpcParams.Receive.SenderClientId))
return;
blockedRocketId = rocket.NetworkObjectId;
blockedRocketColliders = rocket.GetComponentsInChildren<Collider>(true);
unblockAfter = GetNetworkTime() + minimumReinsertBlockDuration;
ClearRocketSlot();
}
private void SetShotDelayOnServer(float seconds)
{
if (!IsServer || State == CannonState.Countdown)
return;
selectedDelay.Value = config != null
? config.ClampDelay(seconds)
: Mathf.Clamp(seconds, 0f, 60f);
}
private bool TryStartCountdownOnServer()
{
if (!IsServer || !IsSpawned || State != CannonState.Loaded ||
LoadedRocket == null || NetworkManager == null)
return false;
fireAt.Value = NetworkManager.ServerTime.Time + selectedDelay.Value;
state.Value = CannonState.Countdown;
return true;
}
private void FireOnServer()
{
NetworkRocketItem rocket = LoadedRocket;
if (rocket == null || rocket.Definition == null)
{
ClearRocketSlot();
return;
}
MissileType missileType = rocket.Definition.MissileType;
mapController?.RequestShot(missileType);
shotShockwave?.EmitShockwave();
lastShot.Value = new CannonShotState(lastShot.Value.Sequence + 1, missileType);
NetworkObject rocketObject = rocket.NetworkObject;
loadedRocket = null;
loadedRocketControl = null;
loadedRocketId.Value = EmptyRocketId;
fireAt.Value = 0d;
state.Value = CannonState.Empty;
if (rocketObject != null && rocketObject.IsSpawned)
rocketObject.Despawn(true);
}
private void ClearRocketSlot()
{
loadedRocket = null;
loadedRocketControl = null;
loadedRocketId.Value = EmptyRocketId;
fireAt.Value = 0d;
state.Value = CannonState.Empty;
}
private void SnapLoadedRocket()
{
if (loadedRocket != null && rocketSnapPoint != null)
{
loadedRocket.transform.SetPositionAndRotation(
rocketSnapPoint.position,
rocketSnapPoint.rotation);
}
}
private NetworkRocketItem ResolveLoadedRocket()
{
if (loadedRocketId.Value == EmptyRocketId || NetworkManager == null ||
NetworkManager.SpawnManager == null ||
!NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(
loadedRocketId.Value, out NetworkObject networkObject))
return null;
loadedRocket = networkObject.GetComponent<NetworkRocketItem>();
return loadedRocket;
}
private IWorkstationItemControl ResolveLoadedControl()
{
NetworkRocketItem rocket = LoadedRocket;
WorkstationItemControlUtility.TryGetControl(
rocket != null ? rocket.GetComponent<NetworkItem>() : null,
out loadedRocketControl);
return loadedRocketControl;
}
private bool TryGetPlayerObject(ulong clientId, out NetworkObject player)
{
player = null;
return NetworkManager != null &&
NetworkManager.ConnectedClients.TryGetValue(clientId, out NetworkClient client) &&
(player = client.PlayerObject) != null;
}
private void UpdateReinsertBlock()
{
if (blockedRocketId == EmptyRocketId || rocketSlot == null ||
GetNetworkTime() < unblockAfter ||
rocketSlot.OverlapsAny(blockedRocketColliders, 0.05f))
return;
blockedRocketId = EmptyRocketId;
blockedRocketColliders = null;
}
private double GetNetworkTime() => NetworkManager != null
? NetworkManager.ServerTime.Time
: Time.timeAsDouble;
private void HandleStateChanged(CannonState _, CannonState __) => Changed?.Invoke();
private void HandleRocketIdChanged(ulong _, ulong __)
{
loadedRocket = null;
loadedRocketControl = null;
Changed?.Invoke();
}
private void HandleDelayChanged(float _, float __) => Changed?.Invoke();
private void HandleFireAtChanged(double _, double __) => Changed?.Invoke();
private void HandleShotChanged(CannonShotState previous, CannonShotState current)
{
if (current.Sequence == previous.Sequence)
return;
ShotFired?.Invoke(current);
Changed?.Invoke();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8382605c6a901154783b2faf23067629
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c09a47e212ae515468d691409560c5d3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 03ef14f0e5db05941afa0b0132e3bfb3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,126 @@
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public sealed class NetworkShockwaveEmitter : NetworkBehaviour
{
private const int MaximumOverlaps = 256;
[SerializeField] private ShockwaveProfile profile;
[SerializeField] private Transform origin;
[SerializeField] private LayerMask physicsLayers = ~0;
[SerializeField] private UnityEvent shockwaveEmitted;
private readonly NetworkVariable<uint> sequence = new(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly Collider[] overlapBuffer = new Collider[MaximumOverlaps];
private readonly HashSet<Rigidbody> affectedBodies = new();
public ShockwaveProfile Profile => profile;
public Vector3 Origin => origin != null ? origin.position : transform.position;
public uint Sequence => sequence.Value;
public event System.Action Emitted;
public override void OnNetworkSpawn()
{
sequence.OnValueChanged += HandleSequenceChanged;
}
public override void OnNetworkDespawn()
{
sequence.OnValueChanged -= HandleSequenceChanged;
}
public bool EmitShockwave() => EmitShockwave(Origin);
public bool EmitShockwave(Vector3 worldOrigin)
{
if (!IsServer || !IsSpawned || profile == null)
return false;
ApplyToPlayers(worldOrigin);
ApplyToPhysics(worldOrigin);
sequence.Value++;
return true;
}
[ContextMenu("Debug/Emit Shockwave (Server Play Mode)")]
private void EmitFromContextMenu()
{
if (Application.isPlaying)
EmitShockwave();
}
private void ApplyToPlayers(Vector3 worldOrigin)
{
foreach (PlayerImpactController player in
FindObjectsByType<PlayerImpactController>(FindObjectsSortMode.None))
{
if (!player.IsSpawned)
continue;
Vector3 offset = player.transform.position - worldOrigin;
float distance = offset.magnitude;
if (!profile.TryEvaluate(distance, out ShockwaveZone zone))
continue;
Vector3 direction = CreateDirection(offset, zone.UpwardBias);
player.ApplyImpactOnServer(direction * zone.PlayerImpulse, zone);
}
}
private void ApplyToPhysics(Vector3 worldOrigin)
{
int count = Physics.OverlapSphereNonAlloc(
worldOrigin,
profile.MaximumRadius,
overlapBuffer,
physicsLayers,
QueryTriggerInteraction.Ignore);
affectedBodies.Clear();
for (int index = 0; index < count; index++)
{
Collider hit = overlapBuffer[index];
Rigidbody body = hit != null ? hit.attachedRigidbody : null;
if (body == null || body.isKinematic || !affectedBodies.Add(body) ||
body.GetComponentInParent<PlayerImpactController>() != null)
{
continue;
}
Vector3 offset = body.worldCenterOfMass - worldOrigin;
if (!profile.TryEvaluate(offset.magnitude, out ShockwaveZone zone) ||
zone.RigidbodyImpulse <= 0f)
{
continue;
}
body.AddForce(
CreateDirection(offset, zone.UpwardBias) * zone.RigidbodyImpulse,
ForceMode.Impulse);
}
}
private static Vector3 CreateDirection(Vector3 offset, float upwardBias)
{
if (offset.sqrMagnitude < 0.0001f)
offset = Vector3.up;
offset.Normalize();
return (offset + Vector3.up * Mathf.Clamp01(upwardBias)).normalized;
}
private void HandleSequenceChanged(uint previous, uint current)
{
if (current == previous)
return;
Emitted?.Invoke();
shockwaveEmitted?.Invoke();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 43f56ec799f9aec4ca1bf372c88592cd
@@ -0,0 +1,110 @@
using System;
using Unity.Netcode;
using UnityEngine;
public struct PlayerImpactState : INetworkSerializable, IEquatable<PlayerImpactState>
{
public uint Sequence;
public Vector3 Impulse;
public float ShakeAmplitude;
public float ShakeDuration;
public float ShakeFrequency;
public float ShakeSeed;
public PlayerImpactState(
uint sequence,
Vector3 impulse,
float shakeAmplitude,
float shakeDuration,
float shakeFrequency,
float shakeSeed)
{
Sequence = sequence;
Impulse = impulse;
ShakeAmplitude = shakeAmplitude;
ShakeDuration = shakeDuration;
ShakeFrequency = shakeFrequency;
ShakeSeed = shakeSeed;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
where T : IReaderWriter
{
serializer.SerializeValue(ref Sequence);
serializer.SerializeValue(ref Impulse);
serializer.SerializeValue(ref ShakeAmplitude);
serializer.SerializeValue(ref ShakeDuration);
serializer.SerializeValue(ref ShakeFrequency);
serializer.SerializeValue(ref ShakeSeed);
}
public bool Equals(PlayerImpactState other) =>
Sequence == other.Sequence && Impulse == other.Impulse &&
ShakeAmplitude.Equals(other.ShakeAmplitude) &&
ShakeDuration.Equals(other.ShakeDuration) &&
ShakeFrequency.Equals(other.ShakeFrequency) &&
ShakeSeed.Equals(other.ShakeSeed);
}
[DisallowMultipleComponent]
public sealed class PlayerImpactController : NetworkBehaviour, IPlayerSystem
{
private readonly NetworkVariable<PlayerImpactState> impactState = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private PlayerController movement;
private PlayerCameraShakeController cameraShake;
public uint LastImpactSequence => impactState.Value.Sequence;
public event Action<PlayerImpactState> ImpactApplied;
public void Initialize(PlayerFacade facade)
{
movement = GetComponent<PlayerController>();
cameraShake = GetComponent<PlayerCameraShakeController>();
}
public override void OnNetworkSpawn()
{
impactState.OnValueChanged += HandleImpactChanged;
}
public override void OnNetworkDespawn()
{
impactState.OnValueChanged -= HandleImpactChanged;
}
public bool ApplyImpactOnServer(Vector3 impulse, ShockwaveZone zone)
{
if (!IsServer || !IsSpawned)
return false;
impactState.Value = new PlayerImpactState(
impactState.Value.Sequence + 1,
impulse,
zone.CameraShakeAmplitude,
zone.CameraShakeDuration,
zone.CameraShakeFrequency,
UnityEngine.Random.Range(0f, 1000f));
return true;
}
private void HandleImpactChanged(PlayerImpactState previous, PlayerImpactState current)
{
if (current.Sequence == 0 || current.Sequence == previous.Sequence)
return;
ImpactApplied?.Invoke(current);
if (!IsOwner)
return;
movement?.AddImpulse(current.Impulse);
cameraShake?.PlayShake(
current.ShakeAmplitude,
current.ShakeDuration,
current.ShakeFrequency,
current.ShakeSeed);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6a7ddedc58d3467449dfbe83af1c9c51
@@ -0,0 +1,57 @@
using System.Collections;
using Unity.Netcode;
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(NetworkRocketItem), typeof(NetworkShockwaveEmitter))]
public sealed class RocketImpactExplosion : NetworkBehaviour
{
[SerializeField] private NetworkRocketItem rocket;
[SerializeField] private NetworkShockwaveEmitter shockwave;
[SerializeField] private bool despawnAfterExplosion = true;
[SerializeField, Min(0f)] private float despawnDelay = 0.15f;
private bool exploded;
private void Reset() => ResolveReferences();
private void Awake() => ResolveReferences();
public override void OnNetworkSpawn()
{
rocket.CriticalImpact += HandleCriticalImpact;
}
public override void OnNetworkDespawn()
{
if (rocket != null)
rocket.CriticalImpact -= HandleCriticalImpact;
}
private void HandleCriticalImpact()
{
if (!IsServer || exploded)
return;
exploded = true;
shockwave.EmitShockwave(rocket.LastCriticalImpactPoint);
if (despawnAfterExplosion)
StartCoroutine(DespawnAfterDelay());
}
private IEnumerator DespawnAfterDelay()
{
if (despawnDelay > 0f)
yield return new WaitForSeconds(despawnDelay);
if (IsServer && NetworkObject != null && NetworkObject.IsSpawned)
NetworkObject.Despawn(true);
}
private void ResolveReferences()
{
if (rocket == null)
rocket = GetComponent<NetworkRocketItem>();
if (shockwave == null)
shockwave = GetComponent<NetworkShockwaveEmitter>();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ac11f143457835f46a1a2defaa624354
@@ -0,0 +1,68 @@
using System;
using UnityEngine;
[Serializable]
public struct ShockwaveZone
{
[Min(0.1f)] public float Radius;
[Min(0f)] public float PlayerImpulse;
[Min(0f)] public float RigidbodyImpulse;
[Range(0f, 1f)] public float UpwardBias;
[Min(0f)] public float CameraShakeAmplitude;
[Min(0f)] public float CameraShakeDuration;
[Min(0.1f)] public float CameraShakeFrequency;
}
[CreateAssetMenu(
fileName = "ShockwaveProfile",
menuName = "FORT-BO/Effects/Shockwave Profile")]
public sealed class ShockwaveProfile : ScriptableObject
{
[Tooltip("Zones are evaluated from the smallest radius to the largest.")]
[SerializeField] private ShockwaveZone[] zones = Array.Empty<ShockwaveZone>();
public float MaximumRadius
{
get
{
float maximum = 0f;
foreach (ShockwaveZone zone in zones)
maximum = Mathf.Max(maximum, zone.Radius);
return maximum;
}
}
public bool TryEvaluate(float distance, out ShockwaveZone result)
{
result = default;
float bestRadius = float.PositiveInfinity;
bool found = false;
foreach (ShockwaveZone zone in zones)
{
if (distance > zone.Radius || zone.Radius >= bestRadius)
continue;
result = zone;
bestRadius = zone.Radius;
found = true;
}
return found;
}
private void OnValidate()
{
for (int index = 0; index < zones.Length; index++)
{
ShockwaveZone zone = zones[index];
zone.Radius = Mathf.Max(0.1f, zone.Radius);
zone.PlayerImpulse = Mathf.Max(0f, zone.PlayerImpulse);
zone.RigidbodyImpulse = Mathf.Max(0f, zone.RigidbodyImpulse);
zone.CameraShakeAmplitude = Mathf.Max(0f, zone.CameraShakeAmplitude);
zone.CameraShakeDuration = Mathf.Max(0f, zone.CameraShakeDuration);
zone.CameraShakeFrequency = Mathf.Max(0.1f, zone.CameraShakeFrequency);
zones[index] = zone;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 96e16f6f88696ea4c8b884f3e7cef9f7
@@ -5,14 +5,20 @@ using UnityEngine.Events;
public sealed class NetworkButtonInteractable : NetworkInteractable
{
[SerializeField] private UnityEvent pressed = new();
[SerializeField] private UnityEvent<bool> buttonStateChanged;
private readonly NetworkVariable<bool> isPressed = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<uint> pressSequence = new(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
public bool IsPressed => isPressed.Value;
public uint PressSequence => pressSequence.Value;
public event Action Pressed;
public event Action<bool> StateChanged;
@@ -20,24 +26,35 @@ public sealed class NetworkButtonInteractable : NetworkInteractable
public override void OnNetworkSpawn()
{
isPressed.OnValueChanged += HandleStateChanged;
pressSequence.OnValueChanged += HandlePressSequenceChanged;
ApplyState(isPressed.Value);
}
public override void OnNetworkDespawn()
{
isPressed.OnValueChanged -= HandleStateChanged;
pressSequence.OnValueChanged -= HandlePressSequenceChanged;
}
protected override bool CanInteractOnServer(ulong senderClientId) => true;
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
{
Pressed?.Invoke();
isPressed.Value = !isPressed.Value;
pressSequence.Value++;
}
private void HandleStateChanged(bool _, bool currentValue) => ApplyState(currentValue);
private void HandlePressSequenceChanged(uint previous, uint current)
{
if (current == previous)
return;
Pressed?.Invoke();
pressed?.Invoke();
}
private void ApplyState(bool currentValue)
{
StateChanged?.Invoke(currentValue);
@@ -404,6 +404,12 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
{
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
if (IsExternallyControlled && !physicsBody.isKinematic)
{
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
}
if (shouldBeKinematic)
{
physicsBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
@@ -417,11 +423,6 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
physicsBody.collisionDetectionMode = ResolveDynamicCollisionDetectionMode();
}
if (IsExternallyControlled)
{
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
}
}
if (HasAnyDragger)
@@ -37,8 +37,7 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
if (!TryGetPlayerObject(senderClientId, out NetworkObject playerObject))
return;
float maximumDistanceSquared = maximumInteractionDistance * maximumInteractionDistance;
if ((playerObject.transform.position - transform.position).sqrMagnitude > maximumDistanceSquared)
if (!IsWithinInteractionDistance(playerObject.transform.position))
return;
if (!CanInteractOnServer(senderClientId))
@@ -47,6 +46,28 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
InteractOnServer(senderClientId, playerObject);
}
private bool IsWithinInteractionDistance(Vector3 playerPosition)
{
float maximumDistanceSquared = maximumInteractionDistance * maximumInteractionDistance;
if ((playerPosition - transform.position).sqrMagnitude <= maximumDistanceSquared)
return true;
foreach (Collider interactionCollider in GetComponentsInChildren<Collider>(true))
{
if (interactionCollider == null || !interactionCollider.enabled ||
!interactionCollider.gameObject.activeInHierarchy)
{
continue;
}
Vector3 closestPoint = interactionCollider.ClosestPoint(playerPosition);
if ((playerPosition - closestPoint).sqrMagnitude <= maximumDistanceSquared)
return true;
}
return false;
}
protected virtual bool CanInteractOnServer(ulong senderClientId) => false;
protected virtual void InteractOnServer(ulong senderClientId, NetworkObject playerObject) { }
@@ -58,4 +79,3 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
return playerObject != null;
}
}
@@ -7,11 +7,14 @@ public sealed class RocketItemDefinition : ItemDefinition
{
[Tooltip("Stable rocket family id used by launchers and orders.")]
[SerializeField] private string rocketTypeId;
[Tooltip("Combat type sent to the map when this rocket is fired.")]
[SerializeField] private MissileType missileType = MissileType.Standard;
[SerializeField, Min(0.1f)] private float criticalImpactSpeed = 7f;
public string RocketTypeId => string.IsNullOrWhiteSpace(rocketTypeId)
? ItemId
: rocketTypeId;
public MissileType MissileType => missileType;
public float CriticalImpactSpeed => criticalImpactSpeed;
protected override void OnValidate()
@@ -0,0 +1,75 @@
using UnityEngine;
[DisallowMultipleComponent]
public sealed class PlayerCameraShakeController : MonoBehaviour, IPlayerSystem
{
private Transform cameraTransform;
private Vector3 baseLocalPosition;
private float amplitude;
private float frequency;
private float seed;
private float endsAt;
private float startedAt;
private float duration;
public bool IsShaking => cameraTransform != null && Time.time < endsAt;
public void Initialize(PlayerFacade facade)
{
FirstPersonCameraController cameraController =
GetComponent<FirstPersonCameraController>();
cameraTransform = cameraController != null && cameraController.PlayerCamera != null
? cameraController.PlayerCamera.transform
: GetComponentInChildren<Camera>(true)?.transform;
if (cameraTransform != null)
baseLocalPosition = cameraTransform.localPosition;
}
public void PlayShake(
float newAmplitude,
float newDuration,
float newFrequency,
float newSeed = 0f)
{
if (cameraTransform == null || newAmplitude <= 0f || newDuration <= 0f)
return;
float remainingStrength = IsShaking
? amplitude * Mathf.Clamp01((endsAt - Time.time) / Mathf.Max(0.01f, duration))
: 0f;
amplitude = Mathf.Max(remainingStrength, newAmplitude);
duration = newDuration;
frequency = Mathf.Max(0.1f, newFrequency);
seed = newSeed;
startedAt = Time.time;
endsAt = Time.time + newDuration;
}
private void LateUpdate()
{
if (cameraTransform == null)
return;
if (!IsShaking)
{
cameraTransform.localPosition = baseLocalPosition;
return;
}
float normalizedTime = Mathf.Clamp01((Time.time - startedAt) / Mathf.Max(0.01f, duration));
float strength = amplitude * (1f - normalizedTime) * (1f - normalizedTime);
float sample = (Time.time - startedAt) * frequency;
Vector3 noise = new(
Mathf.PerlinNoise(seed, sample) * 2f - 1f,
Mathf.PerlinNoise(seed + 17.31f, sample) * 2f - 1f,
Mathf.PerlinNoise(seed + 39.77f, sample) * 2f - 1f);
cameraTransform.localPosition = baseLocalPosition + noise * strength;
}
private void OnDisable()
{
if (cameraTransform != null)
cameraTransform.localPosition = baseLocalPosition;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b12d3c124e93b5f4f9c06c7e870d4068
@@ -13,10 +13,16 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
[SerializeField, Min(0.1f)] private float jumpHeight = 1.4f;
[SerializeField, Min(0.1f)] private float gravity = 20f;
[Header("External impulses")]
[SerializeField, Min(0.1f)] private float impulseDamping = 9f;
private CharacterController characterController;
private Vector3 verticalVelocity;
private Vector3 externalImpulseVelocity;
private bool isInitialized;
public Vector3 ExternalImpulseVelocity => externalImpulseVelocity;
public void Initialize(PlayerFacade facade)
{
characterController = GetComponent<CharacterController>();
@@ -39,6 +45,14 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
transform.Rotate(0f, yawDelta, 0f, Space.Self);
}
public void AddImpulse(Vector3 velocityChange)
{
if (!isInitialized || !IsOwner)
return;
externalImpulseVelocity += velocityChange;
}
private void SimulateMovement(Vector2 moveInput, bool wantsToSprint, bool wantsToJump)
{
if (moveInput.sqrMagnitude > 1f)
@@ -54,6 +68,12 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
float speed = wantsToSprint ? sprintSpeed : walkSpeed;
Vector3 horizontalDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
characterController.Move((horizontalDirection * speed + verticalVelocity) * Time.deltaTime);
characterController.Move(
(horizontalDirection * speed + verticalVelocity + externalImpulseVelocity) *
Time.deltaTime);
externalImpulseVelocity = Vector3.MoveTowards(
externalImpulseVelocity,
Vector3.zero,
impulseDamping * Time.deltaTime);
}
}
@@ -80,6 +80,9 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
for (int hitIndex = 0; hitIndex < hitCount; hitIndex++)
{
RaycastHit hit = interactionHits[hitIndex];
if (hit.collider != null && hit.collider.transform.IsChildOf(transform))
continue;
IInteractable interactable = FindInteractable(hit.collider);
if (interactable == null)
{