第一批重构,主要是物理迁移脚本和部分命名空间的调整
- 结构上迁移文件,将脚本分成 Base/Runtime/Presentation/Procedure/Editor 五大文件夹 - 调整 DataTable 和 Definition 下脚本的命名空间 - 依据 UI-5层架构设计规范 拆分原先的 UI 到 Runtime 和 Presentation 下
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33ba0562010e2da46883baefb271f573
|
||||
folderAsset: yes
|
||||
timeCreated: 1528026146
|
||||
licenseType: Pro
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,77 @@
|
||||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using SepCore.Definition;
|
||||
using GameFramework;
|
||||
using StarForce;
|
||||
using UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class BuiltinDataComponent : GameFrameworkComponent
|
||||
{
|
||||
[SerializeField]
|
||||
private TextAsset m_BuildInfoTextAsset = null;
|
||||
|
||||
[SerializeField]
|
||||
private TextAsset m_DefaultDictionaryTextAsset = null;
|
||||
|
||||
[SerializeField]
|
||||
private UpdateResourceForm m_UpdateResourceFormTemplate = null;
|
||||
|
||||
private BuildInfo m_BuildInfo = null;
|
||||
|
||||
public BuildInfo BuildInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_BuildInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public UpdateResourceForm UpdateResourceFormTemplate
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_UpdateResourceFormTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
public void InitBuildInfo()
|
||||
{
|
||||
if (m_BuildInfoTextAsset == null || string.IsNullOrEmpty(m_BuildInfoTextAsset.text))
|
||||
{
|
||||
Log.Info("Build info can not be found or empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_BuildInfo = Utility.Json.ToObject<BuildInfo>(m_BuildInfoTextAsset.text);
|
||||
if (m_BuildInfo == null)
|
||||
{
|
||||
Log.Warning("Parse build info failure.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void InitDefaultDictionary()
|
||||
{
|
||||
if (m_DefaultDictionaryTextAsset == null || string.IsNullOrEmpty(m_DefaultDictionaryTextAsset.text))
|
||||
{
|
||||
Log.Info("Default dictionary can not be found or empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GameEntry.Localization.ParseData(m_DefaultDictionaryTextAsset.text))
|
||||
{
|
||||
Log.Warning("Parse default dictionary failure.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfb0b001fcb30fe45ac95ddb8c28ba57
|
||||
timeCreated: 1528026146
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6befdfdd5f3e430a864a90bf445000e8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.ObjectPool;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class DamageTextComponent : GameFrameworkComponent
|
||||
{
|
||||
[SerializeField] private int _instancePoolCapacity = 256;
|
||||
|
||||
[SerializeField] private string _poolName = "DamageTextItem";
|
||||
|
||||
[SerializeField] private RectTransform _instanceRoot;
|
||||
|
||||
[SerializeField] private Canvas _canvas;
|
||||
|
||||
[SerializeField] private GameObject _damageTextItemPrefab;
|
||||
|
||||
private IObjectPool<DamageTextItemObject> _damageTextItemPool;
|
||||
|
||||
private readonly List<DamageTextItem> _activeDamageTextItems = new();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_damageTextItemPool =
|
||||
GameEntry.ObjectPool.CreateSingleSpawnObjectPool<DamageTextItemObject>(_poolName,
|
||||
_instancePoolCapacity);
|
||||
}
|
||||
|
||||
public void ShowDamage(Vector3 worldPosition, int damage)
|
||||
{
|
||||
if (damage <= 0 || _damageTextItemPool == null || _canvas == null) return;
|
||||
|
||||
DamageTextItem item = CreateDamageTextItem();
|
||||
if (item == null) return;
|
||||
|
||||
item.Show(worldPosition, damage, _canvas, OnItemComplete);
|
||||
_activeDamageTextItems.Add(item);
|
||||
}
|
||||
|
||||
private DamageTextItem CreateDamageTextItem()
|
||||
{
|
||||
if (_activeDamageTextItems.Count == _instancePoolCapacity)
|
||||
{
|
||||
_instancePoolCapacity = Mathf.Min(_instancePoolCapacity * 2, 1024);
|
||||
_damageTextItemPool.Capacity = _instancePoolCapacity;
|
||||
}
|
||||
|
||||
DamageTextItemObject itemObject = _damageTextItemPool.Spawn();
|
||||
if (itemObject != null)
|
||||
{
|
||||
return (DamageTextItem)itemObject.Target;
|
||||
}
|
||||
|
||||
GameObject itemGo = Instantiate(_damageTextItemPrefab, _instanceRoot, false);
|
||||
|
||||
DamageTextItem item = itemGo.GetComponent<DamageTextItem>();
|
||||
_damageTextItemPool.Register(DamageTextItemObject.Create(item), true);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void OnItemComplete(DamageTextItem item)
|
||||
{
|
||||
if (item == null) return;
|
||||
item.ResetItem();
|
||||
_activeDamageTextItems.Remove(item);
|
||||
_damageTextItemPool.Unspawn(item);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_activeDamageTextItems.Clear();
|
||||
_damageTextItemPool.Release();
|
||||
_damageTextItemPool = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 366eb88b821a4048891b7fb01a1b8d42
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using DG.Tweening;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class DamageTextItem : MonoBehaviour
|
||||
{
|
||||
private const float RiseDistance = 80f;
|
||||
private const float RiseDuration = 0.55f;
|
||||
private const float FadeDelay = 0.2f;
|
||||
private const float FadeDuration = 0.35f;
|
||||
private static readonly Color NormalColor = new Color(1f, 0.32f, 0.23f, 1f);
|
||||
|
||||
[SerializeField] private RectTransform _cachedTransform;
|
||||
|
||||
[SerializeField] private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField] private TMP_Text _text;
|
||||
|
||||
private Sequence _sequence;
|
||||
private Action<DamageTextItem> _onComplete;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_cachedTransform == null) _cachedTransform = GetComponent<RectTransform>();
|
||||
if (_canvasGroup == null) _canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (_text == null) _text = GetComponent<TMP_Text>();
|
||||
}
|
||||
|
||||
public void Show(Vector3 worldPosition, int damage, Canvas canvas, Action<DamageTextItem> onComplete)
|
||||
{
|
||||
if (canvas == null || GameEntry.Scene.MainCamera == null) return;
|
||||
|
||||
_onComplete = onComplete;
|
||||
gameObject.SetActive(true);
|
||||
KillSequence();
|
||||
|
||||
_canvasGroup.alpha = 1f;
|
||||
_cachedTransform.localScale = Vector3.one;
|
||||
_text.color = NormalColor;
|
||||
_text.text = damage.ToString();
|
||||
|
||||
Vector3 screenPos = GameEntry.Scene.MainCamera.WorldToScreenPoint(worldPosition);
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform)canvas.transform, screenPos,
|
||||
canvas.worldCamera, out Vector2 localPos);
|
||||
localPos.x += UnityEngine.Random.Range(-24f, 24f);
|
||||
_cachedTransform.anchoredPosition = localPos;
|
||||
|
||||
_sequence = DOTween.Sequence();
|
||||
_sequence.Append(_cachedTransform.DOAnchorPosY(localPos.y + RiseDistance, RiseDuration)
|
||||
.SetEase(Ease.OutQuad));
|
||||
_sequence.Join(_cachedTransform.DOPunchScale(new Vector3(0.15f, 0.15f, 0f), 0.18f, 6, 0.75f));
|
||||
_sequence.Join(_canvasGroup.DOFade(0f, FadeDuration).SetDelay(FadeDelay));
|
||||
_sequence.OnComplete(() => _onComplete?.Invoke(this));
|
||||
}
|
||||
|
||||
public void ResetItem()
|
||||
{
|
||||
KillSequence();
|
||||
_onComplete = null;
|
||||
_canvasGroup.alpha = 1f;
|
||||
_text.text = string.Empty;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
KillSequence();
|
||||
}
|
||||
|
||||
private void KillSequence()
|
||||
{
|
||||
if (_sequence == null) return;
|
||||
_sequence.Kill();
|
||||
_sequence = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 501e678f9cb14af6ac45e8d2050388d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using GameFramework;
|
||||
using GameFramework.ObjectPool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class DamageTextItemObject : ObjectBase
|
||||
{
|
||||
public static DamageTextItemObject Create(object target)
|
||||
{
|
||||
DamageTextItemObject itemObject = ReferencePool.Acquire<DamageTextItemObject>();
|
||||
itemObject.Initialize(target);
|
||||
return itemObject;
|
||||
}
|
||||
|
||||
protected override void Release(bool isShutdown)
|
||||
{
|
||||
DamageTextItem item = (DamageTextItem)Target;
|
||||
if (item == null) return;
|
||||
Object.Destroy(item.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4aba1230dece448eba1fa7579ca7a13d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a11ef2e359b24bfdb6c9120cc2ec81b8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+527
@@ -0,0 +1,527 @@
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Components;
|
||||
using CustomEvent;
|
||||
using SepCore.DataTable;
|
||||
using SepCore.Definition;
|
||||
using Entity;
|
||||
using CustomUtility;
|
||||
using Procedure;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class RuntimeDebugPanelComponent : GameFrameworkComponent
|
||||
{
|
||||
private const float MinSpawnRate = 0.1f;
|
||||
private const float CornerTapWindow = 0.6f;
|
||||
private const int RequiredCornerTapCount = 3;
|
||||
private const int DebugHealAmount = 200;
|
||||
|
||||
[Header("Window Content")]
|
||||
[SerializeField] private bool _showBuffSection = true;
|
||||
[SerializeField] private bool _showBattleOverview = true;
|
||||
[SerializeField] private bool _showCollisionStats = true;
|
||||
[SerializeField] private bool _showSpawnControls = true;
|
||||
[SerializeField] private bool _showBattleDurationControls = true;
|
||||
[SerializeField] private bool _showPlayerWeaponControls = true;
|
||||
[SerializeField] private bool _showPlayerHealthControls = true;
|
||||
[SerializeField] private bool _showTips = true;
|
||||
|
||||
private Rect _windowRect = new Rect(20f, 60f, 460f, 800f);
|
||||
private bool _isPanelVisible;
|
||||
private int _windowId;
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
private int _selectedIndex;
|
||||
private int _addCount = 1;
|
||||
private Vector2 _buffScroll;
|
||||
|
||||
private float _spawnRateScaleInput = 1f;
|
||||
private float _extendDurationSeconds = 30f;
|
||||
|
||||
private DRProp[] _allProps = Array.Empty<DRProp>();
|
||||
private DRProp[] _filteredProps = Array.Empty<DRProp>();
|
||||
private string[] _displayNames = Array.Empty<string>();
|
||||
|
||||
private int _cornerTapCount;
|
||||
private float _lastCornerTapTime = -10f;
|
||||
private bool _lockPlayerHealthToMax;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
_windowId = GetInstanceID();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (IsTogglePressed())
|
||||
{
|
||||
_isPanelVisible = !_isPanelVisible;
|
||||
}
|
||||
|
||||
HandleCornerTapGesture();
|
||||
if (_lockPlayerHealthToMax)
|
||||
{
|
||||
KeepPlayerHealthAtMax();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
DrawToggleButton();
|
||||
if (!_isPanelVisible) return;
|
||||
|
||||
_windowRect = GUI.Window(_windowId, _windowRect, DrawWindow, "Runtime Debug Panel");
|
||||
}
|
||||
|
||||
private void DrawToggleButton()
|
||||
{
|
||||
const float width = 64f;
|
||||
const float height = 30f;
|
||||
Rect buttonRect = new Rect(Screen.width - width - 12f, 10f, width, height);
|
||||
if (GUI.Button(buttonRect, "DEBUG"))
|
||||
{
|
||||
_isPanelVisible = !_isPanelVisible;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawWindow(int windowId)
|
||||
{
|
||||
if (_showBuffSection)
|
||||
{
|
||||
EnsurePropList();
|
||||
}
|
||||
|
||||
GUILayout.BeginVertical();
|
||||
|
||||
bool hasPreviousSection = false;
|
||||
if (_showBuffSection)
|
||||
{
|
||||
DrawBuffSection();
|
||||
hasPreviousSection = true;
|
||||
}
|
||||
|
||||
if (HasVisibleBattleSection())
|
||||
{
|
||||
if (hasPreviousSection)
|
||||
{
|
||||
GUILayout.Space(8f);
|
||||
GUILayout.Label(string.Empty, GUI.skin.horizontalSlider);
|
||||
GUILayout.Space(8f);
|
||||
}
|
||||
|
||||
DrawBattleSection();
|
||||
hasPreviousSection = true;
|
||||
}
|
||||
|
||||
if (_showTips)
|
||||
{
|
||||
if (hasPreviousSection)
|
||||
{
|
||||
GUILayout.Space(8f);
|
||||
}
|
||||
|
||||
GUILayout.Label("Tips: press `F8` or tap top-left corner 3 times to toggle.", GUILayout.Height(20f));
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUI.DragWindow(new Rect(0, 0, 10000, 22));
|
||||
}
|
||||
|
||||
private void DrawBuffSection()
|
||||
{
|
||||
GUILayout.Label("Buff Debug");
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Search", GUILayout.Width(52f));
|
||||
_searchText = GUILayout.TextField(_searchText);
|
||||
if (GUILayout.Button("Refresh", GUILayout.Width(80f)))
|
||||
{
|
||||
EnsurePropList(true);
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
ApplyFilter(_searchText);
|
||||
if (_displayNames.Length == 0)
|
||||
{
|
||||
GUILayout.Label("No Buff data. Enter battle and try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedIndex = Mathf.Clamp(_selectedIndex, 0, _displayNames.Length - 1);
|
||||
_buffScroll = GUILayout.BeginScrollView(_buffScroll, GUILayout.Height(120f));
|
||||
_selectedIndex = GUILayout.SelectionGrid(_selectedIndex, _displayNames, 1);
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
DRProp selectedProp = GetSelectedProp();
|
||||
if (selectedProp == null) return;
|
||||
|
||||
GUILayout.Label($"Selected: {selectedProp.Title} ({selectedProp.Rarity})");
|
||||
GUILayout.Label(ItemDescUtility.CreatePropDescription(selectedProp.Modifiers), GUILayout.Height(40f));
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Count", GUILayout.Width(52f));
|
||||
string addCountText = GUILayout.TextField(_addCount.ToString(), GUILayout.Width(60f));
|
||||
if (!int.TryParse(addCountText, out _addCount)) _addCount = 1;
|
||||
_addCount = Mathf.Clamp(_addCount, 1, 99);
|
||||
|
||||
if (GUILayout.Button("Add Buff To Player", GUILayout.Height(24f)))
|
||||
{
|
||||
AddSelectedBuffToPlayer(selectedProp, _addCount);
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DrawBattleSection()
|
||||
{
|
||||
GUILayout.Label("Battle Debug");
|
||||
ProcedureGame procedure = GameEntry.Procedure.CurrentProcedure as ProcedureGame;
|
||||
EnemyManagerComponent enemyManager = GameEntry.EnemyManager;
|
||||
Player player = FindPlayer();
|
||||
HealthComponent playerHealth = player != null ? player.GetComponent<HealthComponent>() : null;
|
||||
|
||||
if (enemyManager == null)
|
||||
{
|
||||
GUILayout.Label("EnemyManager unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (procedure == null)
|
||||
{
|
||||
GUILayout.Label("ProcedureGame unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_showBattleOverview)
|
||||
{
|
||||
GUILayout.Label($"Spawn Rate: {enemyManager.SpawnRateScale:F2}");
|
||||
GUILayout.Label($"Battle Time: {enemyManager.ElapsedBattleTime:F1}s / {enemyManager.BattleDuration:F1}s");
|
||||
GUILayout.Label($"Enemy Count: {enemyManager.CurrentEnemyCount}");
|
||||
}
|
||||
|
||||
Simulation.SimulationWorld simulationWorld = GameEntry.SimulationWorld;
|
||||
if (_showCollisionStats && simulationWorld != null)
|
||||
{
|
||||
GUILayout.Space(4f);
|
||||
GUILayout.Label(
|
||||
$"Collision Queries: total {simulationWorld.LastCollisionQueryCount} (Projectile {simulationWorld.LastProjectileCollisionQueryCount} / Area {simulationWorld.LastAreaCollisionQueryCount})");
|
||||
GUILayout.Label(
|
||||
$"Collision Candidates: total {simulationWorld.LastCollisionCandidateCount} (Projectile {simulationWorld.LastProjectileCollisionCandidateCount} / Area {simulationWorld.LastAreaCollisionCandidateCount})");
|
||||
GUILayout.Label(
|
||||
$"Area Resolve: hits {simulationWorld.LastResolvedAreaHitCount}");
|
||||
GUILayout.Label(
|
||||
$"Broad Phase: cell {simulationWorld.LastCollisionCellSize:F2}, hasEnemyTargets {(simulationWorld.LastCollisionHasEnemyTargets ? "Yes" : "No")}");
|
||||
if (simulationWorld.LastCollisionCandidateCount != 0)
|
||||
{
|
||||
Log.Info($"LastCollisionCandidateCount:{simulationWorld.LastCollisionCandidateCount}");
|
||||
}
|
||||
|
||||
if (simulationWorld.LastResolvedAreaHitCount != 0)
|
||||
{
|
||||
Log.Info($"LastResolvedAreaHitCount:{simulationWorld.LastResolvedAreaHitCount}");
|
||||
}
|
||||
}
|
||||
|
||||
if (_showSpawnControls)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Rate", GUILayout.Width(52f));
|
||||
string rateText = GUILayout.TextField(_spawnRateScaleInput.ToString("F2"), GUILayout.Width(60f));
|
||||
if (float.TryParse(rateText, out float parsedRate))
|
||||
{
|
||||
_spawnRateScaleInput = Mathf.Clamp(parsedRate, MinSpawnRate, 50f);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Apply", GUILayout.Width(70f)))
|
||||
{
|
||||
enemyManager.SetSpawnRateScale(_spawnRateScaleInput);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("x0.5", GUILayout.Width(60f)))
|
||||
{
|
||||
_spawnRateScaleInput = Mathf.Max(MinSpawnRate, enemyManager.SpawnRateScale * 0.5f);
|
||||
enemyManager.SetSpawnRateScale(_spawnRateScaleInput);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("x2", GUILayout.Width(60f)))
|
||||
{
|
||||
_spawnRateScaleInput = enemyManager.SpawnRateScale * 2f;
|
||||
enemyManager.SetSpawnRateScale(_spawnRateScaleInput);
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
if (_showBattleDurationControls)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Add Sec", GUILayout.Width(52f));
|
||||
string durationText = GUILayout.TextField(_extendDurationSeconds.ToString("F0"), GUILayout.Width(60f));
|
||||
if (float.TryParse(durationText, out float parsedDuration))
|
||||
{
|
||||
_extendDurationSeconds = Mathf.Clamp(parsedDuration, 1f, 3600f);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Extend Battle", GUILayout.Height(24f)))
|
||||
{
|
||||
if (procedure.CurrentGameState is GameStateBattle gameState)
|
||||
{
|
||||
gameState.AddBattleDuration(_extendDurationSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
if (_showPlayerWeaponControls)
|
||||
{
|
||||
GUILayout.Label(
|
||||
$"Player Weapon: {(player == null ? "Player not found" : (player.WeaponEnabled ? "Enabled" : "Disabled"))}");
|
||||
GUILayout.BeginHorizontal();
|
||||
GUI.enabled = player != null;
|
||||
if (GUILayout.Button("Disable Weapons", GUILayout.Height(24f)))
|
||||
{
|
||||
player.SetWeaponEnabled(false);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Enable Weapons", GUILayout.Height(24f)))
|
||||
{
|
||||
player.SetWeaponEnabled(true);
|
||||
}
|
||||
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
if (_showPlayerHealthControls)
|
||||
{
|
||||
GUILayout.Space(4f);
|
||||
GUILayout.Label(
|
||||
$"Player HP: {(playerHealth == null ? "Unavailable" : $"{playerHealth.CurrentHealth}/{playerHealth.MaxHealth}")}");
|
||||
GUILayout.BeginHorizontal();
|
||||
GUI.enabled = playerHealth != null;
|
||||
if (GUILayout.Button($"+{DebugHealAmount} HP", GUILayout.Height(24f)))
|
||||
{
|
||||
AddPlayerHealth(playerHealth, DebugHealAmount);
|
||||
}
|
||||
|
||||
if (GUILayout.Button(_lockPlayerHealthToMax ? "GodMode: ON" : "GodMode: OFF", GUILayout.Height(24f)))
|
||||
{
|
||||
_lockPlayerHealthToMax = !_lockPlayerHealthToMax;
|
||||
if (_lockPlayerHealthToMax)
|
||||
{
|
||||
RestorePlayerHealthToMax(playerHealth);
|
||||
}
|
||||
}
|
||||
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasVisibleBattleSection()
|
||||
{
|
||||
return _showBattleOverview ||
|
||||
_showCollisionStats ||
|
||||
_showSpawnControls ||
|
||||
_showBattleDurationControls ||
|
||||
_showPlayerWeaponControls ||
|
||||
_showPlayerHealthControls;
|
||||
}
|
||||
|
||||
private void EnsurePropList(bool force = false)
|
||||
{
|
||||
if (!force && _allProps != null && _allProps.Length > 0) return;
|
||||
|
||||
if (GameEntry.DataTable == null)
|
||||
{
|
||||
_allProps = Array.Empty<DRProp>();
|
||||
_filteredProps = Array.Empty<DRProp>();
|
||||
_displayNames = Array.Empty<string>();
|
||||
return;
|
||||
}
|
||||
|
||||
var table = GameEntry.DataTable.GetDataTable<DRProp>();
|
||||
_allProps = table != null ? table.ToArray() : Array.Empty<DRProp>();
|
||||
_selectedIndex = Mathf.Clamp(_selectedIndex, 0, Mathf.Max(0, _allProps.Length - 1));
|
||||
ApplyFilter(_searchText);
|
||||
}
|
||||
|
||||
private void ApplyFilter(string keyword)
|
||||
{
|
||||
if (_allProps == null || _allProps.Length == 0)
|
||||
{
|
||||
_filteredProps = Array.Empty<DRProp>();
|
||||
_displayNames = Array.Empty<string>();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
_filteredProps = _allProps;
|
||||
}
|
||||
else
|
||||
{
|
||||
string search = keyword.Trim();
|
||||
_filteredProps = _allProps.Where(p =>
|
||||
p != null &&
|
||||
(p.Title?.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
p.Id.ToString().Contains(search))).ToArray();
|
||||
}
|
||||
|
||||
_displayNames = _filteredProps.Select(p => $"[{p.Id}] {p.Title} ({p.Rarity})").ToArray();
|
||||
if (_displayNames.Length == 0) _selectedIndex = 0;
|
||||
else _selectedIndex = Mathf.Clamp(_selectedIndex, 0, _displayNames.Length - 1);
|
||||
}
|
||||
|
||||
private DRProp GetSelectedProp()
|
||||
{
|
||||
if (_filteredProps == null || _filteredProps.Length == 0) return null;
|
||||
_selectedIndex = Mathf.Clamp(_selectedIndex, 0, _filteredProps.Length - 1);
|
||||
return _filteredProps[_selectedIndex];
|
||||
}
|
||||
|
||||
private static Player FindPlayer()
|
||||
{
|
||||
return UnityEngine.Object.FindObjectOfType<Player>();
|
||||
}
|
||||
|
||||
private void KeepPlayerHealthAtMax()
|
||||
{
|
||||
Player player = FindPlayer();
|
||||
if (player == null) return;
|
||||
|
||||
HealthComponent playerHealth = player.GetComponent<HealthComponent>();
|
||||
if (playerHealth == null) return;
|
||||
|
||||
RestorePlayerHealthToMax(playerHealth);
|
||||
}
|
||||
|
||||
private static void AddPlayerHealth(HealthComponent playerHealth, int amount)
|
||||
{
|
||||
if (playerHealth == null || amount <= 0) return;
|
||||
if (playerHealth.CurrentHealth <= 0) return;
|
||||
|
||||
int maxHealth = playerHealth.MaxHealth;
|
||||
if (maxHealth <= 0) return;
|
||||
|
||||
int nextHealth = Mathf.Clamp(playerHealth.CurrentHealth + amount, 0, maxHealth);
|
||||
if (nextHealth == playerHealth.CurrentHealth) return;
|
||||
|
||||
playerHealth.CurrentHealth = nextHealth;
|
||||
PublishPlayerHealthChanged(playerHealth);
|
||||
}
|
||||
|
||||
private static void RestorePlayerHealthToMax(HealthComponent playerHealth)
|
||||
{
|
||||
if (playerHealth == null) return;
|
||||
if (playerHealth.CurrentHealth <= 0) return;
|
||||
|
||||
int maxHealth = playerHealth.MaxHealth;
|
||||
if (maxHealth <= 0 || playerHealth.CurrentHealth >= maxHealth) return;
|
||||
|
||||
playerHealth.CurrentHealth = maxHealth;
|
||||
PublishPlayerHealthChanged(playerHealth);
|
||||
}
|
||||
|
||||
private static void PublishPlayerHealthChanged(HealthComponent playerHealth)
|
||||
{
|
||||
if (playerHealth == null || GameEntry.Event == null) return;
|
||||
|
||||
GameEntry.Event.Fire(null,
|
||||
PlayerHealthChangeEventArgs.Create(0, playerHealth.CurrentHealth, playerHealth.MaxHealth));
|
||||
}
|
||||
|
||||
private static void AddSelectedBuffToPlayer(DRProp prop, int count)
|
||||
{
|
||||
Player player = FindPlayer();
|
||||
if (player == null || prop == null || prop.Modifiers == null) return;
|
||||
|
||||
int applyCount = Mathf.Clamp(count, 1, 99);
|
||||
for (int i = 0; i < applyCount; i++)
|
||||
{
|
||||
player.AddProp(new PropItem(prop.Modifiers, prop.Rarity, prop.Title, prop.IconAssetName));
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCornerTapGesture()
|
||||
{
|
||||
if (!TryGetTouchReleased(out Vector2 touchPosition)) return;
|
||||
if (touchPosition.x > 80f || touchPosition.y < Screen.height - 80f) return;
|
||||
|
||||
float now = Time.unscaledTime;
|
||||
if (now - _lastCornerTapTime > CornerTapWindow)
|
||||
{
|
||||
_cornerTapCount = 0;
|
||||
}
|
||||
|
||||
_lastCornerTapTime = now;
|
||||
_cornerTapCount++;
|
||||
|
||||
if (_cornerTapCount >= RequiredCornerTapCount)
|
||||
{
|
||||
_cornerTapCount = 0;
|
||||
_isPanelVisible = !_isPanelVisible;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTogglePressed()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
Keyboard keyboard = Keyboard.current;
|
||||
if (keyboard == null) return false;
|
||||
return keyboard.backquoteKey.wasPressedThisFrame || keyboard.f8Key.wasPressedThisFrame;
|
||||
#else
|
||||
return Input.GetKeyDown(KeyCode.BackQuote) || Input.GetKeyDown(KeyCode.F8);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static bool TryGetTouchReleased(out Vector2 touchPosition)
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
Touchscreen touchscreen = Touchscreen.current;
|
||||
if (touchscreen == null)
|
||||
{
|
||||
touchPosition = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var touch = touchscreen.primaryTouch;
|
||||
if (!touch.press.wasReleasedThisFrame)
|
||||
{
|
||||
touchPosition = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
touchPosition = touch.position.ReadValue();
|
||||
return true;
|
||||
#else
|
||||
if (Input.touchCount <= 0)
|
||||
{
|
||||
touchPosition = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
Touch touch = Input.GetTouch(0);
|
||||
if (touch.phase != TouchPhase.Ended)
|
||||
{
|
||||
touchPosition = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
touchPosition = touch.position;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d8ada5157a04921a6e543a040e57960
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 259253e364fe32a4195e182f78b8cc54
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
using System.Collections.Generic;
|
||||
using SepCore.DataTable;
|
||||
using SepCore.Definition;
|
||||
using Entity;
|
||||
using Entity.EntityData;
|
||||
using GameFramework.Event;
|
||||
using Procedure;
|
||||
using StarForce;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class EnemyManagerComponent : GameFrameworkComponent
|
||||
{
|
||||
private const float MinSpawnRateScale = 0.1f;
|
||||
private const string EnemyGroupName = "Enemy";
|
||||
|
||||
private EntityComponent _entity;
|
||||
|
||||
private List<EntityBase> _enemies;
|
||||
private Dictionary<int, EntityBase> _enemyById;
|
||||
|
||||
public List<EntityBase> Enemies => _enemies;
|
||||
|
||||
private float _spawnEnemyTimer;
|
||||
|
||||
[SerializeField] private int _spawnEnemyMaxCount = 5000;
|
||||
|
||||
private int _currentEnemyCount;
|
||||
|
||||
[SerializeField] private int _spawnDistanceFromPlayer = 20;
|
||||
|
||||
private int _currentSpawnEnemyId;
|
||||
|
||||
private int _currentLevel;
|
||||
|
||||
private float[] _baseSpawnEnemyIntervals;
|
||||
private float[] _spawnEnemyIntervals;
|
||||
private EnemyType[] _spawnEnemyTypes;
|
||||
private int[] _spawnEnemyCounts;
|
||||
private float _duration;
|
||||
private float _baseDuration;
|
||||
|
||||
private float[] _nextSpawnTimes;
|
||||
private float _spawnRateScale = 1f;
|
||||
|
||||
private Transform _player;
|
||||
|
||||
public float SpawnRateScale => _spawnRateScale;
|
||||
public float BattleDuration => _duration;
|
||||
public float ElapsedBattleTime => _spawnEnemyTimer;
|
||||
public int CurrentEnemyCount => _currentEnemyCount;
|
||||
|
||||
#region FSM
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_entity = GameEntry.Entity;
|
||||
_enemies = new List<EntityBase>();
|
||||
_enemyById = new Dictionary<int, EntityBase>();
|
||||
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (GameEntry.Event != null)
|
||||
{
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
}
|
||||
|
||||
_enemies = null;
|
||||
_enemyById = null;
|
||||
_entity = null;
|
||||
}
|
||||
|
||||
public void OnInit(DRLevel level)
|
||||
{
|
||||
_baseSpawnEnemyIntervals = (float[])level.Intervals.Clone();
|
||||
_spawnEnemyIntervals = (float[])_baseSpawnEnemyIntervals.Clone();
|
||||
_spawnEnemyTypes = level.EntityTypes;
|
||||
_spawnEnemyCounts = level.EntityCounts;
|
||||
_baseDuration = level.Duration;
|
||||
_duration = _baseDuration;
|
||||
|
||||
SetSpawnRateScale(_spawnRateScale);
|
||||
|
||||
_currentEnemyCount = 0;
|
||||
_currentSpawnEnemyId = 0;
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_spawnEnemyTimer += elapseSeconds;
|
||||
|
||||
for (int i = 0; i < _nextSpawnTimes.Length; i++)
|
||||
{
|
||||
float nextSpawnTime = _nextSpawnTimes[i];
|
||||
if (_spawnEnemyTimer < nextSpawnTime) continue;
|
||||
for (int j = 0; j < _spawnEnemyCounts[i]; j++)
|
||||
{
|
||||
SpawnEnemy(_spawnEnemyTypes[i]);
|
||||
}
|
||||
|
||||
_nextSpawnTimes[i] += _spawnEnemyIntervals[i];
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_spawnEnemyTimer = 0;
|
||||
_currentSpawnEnemyId = 0;
|
||||
_currentLevel = 0;
|
||||
|
||||
_baseSpawnEnemyIntervals = null;
|
||||
_spawnEnemyIntervals = null;
|
||||
_spawnEnemyTypes = null;
|
||||
_spawnEnemyCounts = null;
|
||||
_baseDuration = 0;
|
||||
_duration = 0;
|
||||
|
||||
_nextSpawnTimes = null;
|
||||
|
||||
ClearEnemies();
|
||||
|
||||
_currentEnemyCount = 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void SpawnEnemy(EnemyType enemyType)
|
||||
{
|
||||
if (_player == null) return;
|
||||
|
||||
if (_currentEnemyCount >= _spawnEnemyMaxCount) return;
|
||||
int entityPoolId = _currentSpawnEnemyId % _spawnEnemyMaxCount;
|
||||
var enemyData = new EnemyData(entityPoolId, enemyType, _currentLevel)
|
||||
{
|
||||
Position = GetRandomPosition()
|
||||
};
|
||||
_entity.ShowEnemy(enemyData);
|
||||
_currentSpawnEnemyId++;
|
||||
}
|
||||
|
||||
private Vector3 GetRandomPosition()
|
||||
{
|
||||
float x = Random.Range(-1f, 1f);
|
||||
float z = Random.Range(-1f, 1f);
|
||||
Vector3 dir = new Vector3(x, 0, z).normalized;
|
||||
return _player.position + dir * _spawnDistanceFromPlayer;
|
||||
}
|
||||
|
||||
public void ClearEnemies()
|
||||
{
|
||||
foreach (var enemy in _enemies)
|
||||
{
|
||||
if (enemy == null || !enemy.Available) continue;
|
||||
_entity.HideEntity(enemy);
|
||||
}
|
||||
|
||||
_enemies.Clear();
|
||||
_enemyById?.Clear();
|
||||
}
|
||||
|
||||
public bool TryGetEnemy(int entityId, out EntityBase enemy)
|
||||
{
|
||||
enemy = null;
|
||||
if (_enemyById == null || !_enemyById.TryGetValue(entityId, out EntityBase cachedEnemy))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cachedEnemy == null || !cachedEnemy.Available)
|
||||
{
|
||||
_enemyById.Remove(entityId);
|
||||
return false;
|
||||
}
|
||||
|
||||
enemy = cachedEnemy;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetSpawnRateScale(float scale)
|
||||
{
|
||||
float newScale = Mathf.Max(MinSpawnRateScale, scale);
|
||||
if (_baseSpawnEnemyIntervals == null || _baseSpawnEnemyIntervals.Length == 0)
|
||||
{
|
||||
_spawnRateScale = newScale;
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasRuntimeState = _nextSpawnTimes != null && _spawnEnemyIntervals != null &&
|
||||
_nextSpawnTimes.Length == _baseSpawnEnemyIntervals.Length &&
|
||||
_spawnEnemyIntervals.Length == _baseSpawnEnemyIntervals.Length;
|
||||
float oldScale = _spawnRateScale;
|
||||
_spawnRateScale = newScale;
|
||||
|
||||
if (!hasRuntimeState)
|
||||
{
|
||||
for (int i = 0; i < _baseSpawnEnemyIntervals.Length; i++)
|
||||
{
|
||||
_spawnEnemyIntervals[i] = GetScaledInterval(_baseSpawnEnemyIntervals[i], _spawnRateScale);
|
||||
}
|
||||
|
||||
_nextSpawnTimes = (float[])_spawnEnemyIntervals.Clone();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _baseSpawnEnemyIntervals.Length; i++)
|
||||
{
|
||||
float oldInterval = GetScaledInterval(_baseSpawnEnemyIntervals[i], oldScale);
|
||||
float newInterval = GetScaledInterval(_baseSpawnEnemyIntervals[i], _spawnRateScale);
|
||||
|
||||
float remainTime = Mathf.Max(0f, _nextSpawnTimes[i] - _spawnEnemyTimer);
|
||||
float remainRatio = oldInterval > Mathf.Epsilon ? Mathf.Clamp01(remainTime / oldInterval) : 0f;
|
||||
|
||||
_spawnEnemyIntervals[i] = newInterval;
|
||||
_nextSpawnTimes[i] = _spawnEnemyTimer + newInterval * remainRatio;
|
||||
}
|
||||
}
|
||||
|
||||
private static float GetScaledInterval(float baseInterval, float scale)
|
||||
{
|
||||
float safeScale = Mathf.Max(MinSpawnRateScale, scale);
|
||||
return baseInterval / safeScale;
|
||||
}
|
||||
|
||||
#region Event Handler
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is ShowEntitySuccessEventArgs ne)) return;
|
||||
|
||||
string entityGroupName = ne.Entity?.EntityGroup?.Name;
|
||||
|
||||
if (entityGroupName == EnemyGroupName && ne.Entity.Logic is EnemyBase enemy)
|
||||
{
|
||||
_currentEnemyCount++;
|
||||
enemy.SetTarget(_player);
|
||||
RemoveEnemyFromCache(enemy.Id);
|
||||
_enemies.Add(enemy);
|
||||
_enemyById[enemy.Id] = enemy;
|
||||
}
|
||||
|
||||
if (ne.EntityLogicType == typeof(Player))
|
||||
{
|
||||
_player = ne.Entity.transform;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is HideEntityCompleteEventArgs ne)
|
||||
{
|
||||
string entityGroupName = ne.EntityGroup.Name;
|
||||
if (entityGroupName == EnemyGroupName)
|
||||
{
|
||||
if (_currentEnemyCount > 0)
|
||||
{
|
||||
_currentEnemyCount--;
|
||||
}
|
||||
|
||||
RemoveEnemyFromCache(ne.EntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEnemyFromCache(int entityId)
|
||||
{
|
||||
if (_enemyById != null)
|
||||
{
|
||||
_enemyById.Remove(entityId);
|
||||
}
|
||||
|
||||
for (int i = _enemies.Count - 1; i >= 0; i--)
|
||||
{
|
||||
EntityBase cachedEnemy = _enemies[i];
|
||||
if (cachedEnemy == null || cachedEnemy.Id == entityId)
|
||||
{
|
||||
if (cachedEnemy != null && _enemyById != null)
|
||||
{
|
||||
_enemyById.Remove(cachedEnemy.Id);
|
||||
}
|
||||
|
||||
_enemies.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d840df1b53db744cb522b82426f2cd5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f75cb14d28565904e89eb5765829c210
|
||||
folderAsset: yes
|
||||
timeCreated: 1528026155
|
||||
licenseType: Pro
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,125 @@
|
||||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework.ObjectPool;
|
||||
using System.Collections.Generic;
|
||||
using Entity;
|
||||
using StarForce;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace CustomComponent
|
||||
{
|
||||
public class HPBarComponent : GameFrameworkComponent
|
||||
{
|
||||
[SerializeField] private HPBarItem _hpBarItemTemplate = null;
|
||||
|
||||
[SerializeField] private Transform _hpBarInstanceRoot = null;
|
||||
|
||||
[SerializeField] private int _instancePoolCapacity = 16;
|
||||
|
||||
private IObjectPool<HPBarItemObject> _hpBarItemObjectPool = null;
|
||||
private List<HPBarItem> _activeHPBarItems = null;
|
||||
private Canvas _cachedCanvas = null;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_hpBarInstanceRoot == null)
|
||||
{
|
||||
Log.Error("You must set HP bar instance root first.");
|
||||
return;
|
||||
}
|
||||
|
||||
_cachedCanvas = _hpBarInstanceRoot.GetComponent<Canvas>();
|
||||
_hpBarItemObjectPool =
|
||||
GameEntry.ObjectPool.CreateSingleSpawnObjectPool<HPBarItemObject>("HPBarItem", _instancePoolCapacity);
|
||||
_activeHPBarItems = new List<HPBarItem>();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
for (int i = _activeHPBarItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
HPBarItem hpBarItem = _activeHPBarItems[i];
|
||||
if (hpBarItem.Refresh())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
HideHPBar(hpBarItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowHPBar(EntityBase entity, float fromHPRatio, float toHPRatio)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
Log.Warning("Entity is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
HPBarItem hpBarItem = GetActiveHPBarItem(entity);
|
||||
if (hpBarItem == null)
|
||||
{
|
||||
hpBarItem = CreateHPBarItem(entity);
|
||||
_activeHPBarItems.Add(hpBarItem);
|
||||
}
|
||||
|
||||
hpBarItem.Init(entity, _cachedCanvas, fromHPRatio, toHPRatio);
|
||||
}
|
||||
|
||||
private void HideHPBar(HPBarItem hpBarItem)
|
||||
{
|
||||
hpBarItem.Reset();
|
||||
_activeHPBarItems.Remove(hpBarItem);
|
||||
_hpBarItemObjectPool.Unspawn(hpBarItem);
|
||||
}
|
||||
|
||||
private HPBarItem GetActiveHPBarItem(EntityBase entity)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _activeHPBarItems.Count; i++)
|
||||
{
|
||||
if (_activeHPBarItems[i].Owner == entity)
|
||||
{
|
||||
return _activeHPBarItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HPBarItem CreateHPBarItem(EntityBase entity)
|
||||
{
|
||||
HPBarItem hpBarItem = null;
|
||||
HPBarItemObject hpBarItemObject = _hpBarItemObjectPool.Spawn();
|
||||
if (hpBarItemObject != null)
|
||||
{
|
||||
hpBarItem = (HPBarItem)hpBarItemObject.Target;
|
||||
}
|
||||
else
|
||||
{
|
||||
hpBarItem = Instantiate(_hpBarItemTemplate);
|
||||
Transform transform = hpBarItem.GetComponent<Transform>();
|
||||
transform.SetParent(_hpBarInstanceRoot);
|
||||
transform.localScale = Vector3.one;
|
||||
_hpBarItemObjectPool.Register(HPBarItemObject.Create(hpBarItem), true);
|
||||
}
|
||||
|
||||
return hpBarItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 879e921586721514f9f85de777050af4
|
||||
timeCreated: 1528026155
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,122 @@
|
||||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using System.Collections;
|
||||
using Entity;
|
||||
using UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace StarForce
|
||||
{
|
||||
public class HPBarItem : MonoBehaviour
|
||||
{
|
||||
private const float AnimationSeconds = 0.3f;
|
||||
private const float KeepSeconds = 0.4f;
|
||||
private const float FadeOutSeconds = 0.3f;
|
||||
|
||||
[SerializeField]
|
||||
private Slider m_HPBar = null;
|
||||
|
||||
private Canvas m_ParentCanvas = null;
|
||||
private RectTransform m_CachedTransform = null;
|
||||
private CanvasGroup m_CachedCanvasGroup = null;
|
||||
private EntityBase m_Owner = null;
|
||||
private int m_OwnerId = 0;
|
||||
|
||||
public EntityBase Owner
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_Owner;
|
||||
}
|
||||
}
|
||||
|
||||
public void Init(EntityBase owner, Canvas parentCanvas, float fromHPRatio, float toHPRatio)
|
||||
{
|
||||
if (owner == null)
|
||||
{
|
||||
Log.Error("Owner is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_ParentCanvas = parentCanvas;
|
||||
|
||||
gameObject.SetActive(true);
|
||||
StopAllCoroutines();
|
||||
|
||||
m_CachedCanvasGroup.alpha = 1f;
|
||||
if (m_Owner != owner || m_OwnerId != owner.Id)
|
||||
{
|
||||
m_HPBar.value = fromHPRatio;
|
||||
m_Owner = owner;
|
||||
m_OwnerId = owner.Id;
|
||||
}
|
||||
|
||||
Refresh();
|
||||
|
||||
StartCoroutine(HPBarCo(toHPRatio, AnimationSeconds, KeepSeconds, FadeOutSeconds));
|
||||
}
|
||||
|
||||
public bool Refresh()
|
||||
{
|
||||
if (m_CachedCanvasGroup.alpha <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Owner != null && Owner.Available && Owner.Id == m_OwnerId)
|
||||
{
|
||||
Vector3 worldPosition = m_Owner.CachedTransform.position + Vector3.forward;
|
||||
Vector3 screenPosition = GameEntry.Scene.MainCamera.WorldToScreenPoint(worldPosition);
|
||||
|
||||
Vector2 position;
|
||||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform)m_ParentCanvas.transform, screenPosition,
|
||||
m_ParentCanvas.worldCamera, out position))
|
||||
{
|
||||
m_CachedTransform.localPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
m_CachedCanvasGroup.alpha = 1f;
|
||||
m_HPBar.value = 1f;
|
||||
m_Owner = null;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
m_CachedTransform = GetComponent<RectTransform>();
|
||||
if (m_CachedTransform == null)
|
||||
{
|
||||
Log.Error("RectTransform is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_CachedCanvasGroup = GetComponent<CanvasGroup>();
|
||||
if (m_CachedCanvasGroup == null)
|
||||
{
|
||||
Log.Error("CanvasGroup is invalid.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator HPBarCo(float value, float animationDuration, float keepDuration, float fadeOutDuration)
|
||||
{
|
||||
yield return m_HPBar.SmoothValue(value, animationDuration);
|
||||
yield return new WaitForSeconds(keepDuration);
|
||||
yield return m_CachedCanvasGroup.FadeToAlpha(0f, fadeOutDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bcc0608685184647b097f4838a8e9f6
|
||||
timeCreated: 1528026155
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework;
|
||||
using GameFramework.ObjectPool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace StarForce
|
||||
{
|
||||
public class HPBarItemObject : ObjectBase
|
||||
{
|
||||
public static HPBarItemObject Create(object target)
|
||||
{
|
||||
HPBarItemObject hpBarItemObject = ReferencePool.Acquire<HPBarItemObject>();
|
||||
hpBarItemObject.Initialize(target);
|
||||
return hpBarItemObject;
|
||||
}
|
||||
|
||||
protected override void Release(bool isShutdown)
|
||||
{
|
||||
HPBarItem hpBarItem = (HPBarItem)Target;
|
||||
if (hpBarItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Object.Destroy(hpBarItem.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e6d2d8655da0094bb6063ce38ed582d
|
||||
timeCreated: 1528026155
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f519a6e4d7b4cb4ab5eabf95fb55e1b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f28ff313e954720a04246191b7f9ddd
|
||||
timeCreated: 1771901064
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Collision Transient
|
||||
|
||||
public int CollisionCandidateCount => _collisionCandidates.IsCreated ? _collisionCandidates.Length : 0;
|
||||
public int PendingAreaCollisionRequestCount => _areaCollisionRequests.Count;
|
||||
public int LastCollisionQueryCount => _lastCollisionQueryCount;
|
||||
public int LastProjectileCollisionQueryCount => _lastProjectileCollisionQueryCount;
|
||||
public int LastAreaCollisionQueryCount => _lastAreaCollisionQueryCount;
|
||||
public int LastCollisionCandidateCount => _lastCollisionCandidateCount;
|
||||
public int LastProjectileCollisionCandidateCount => _lastProjectileCollisionCandidateCount;
|
||||
public int LastAreaCollisionCandidateCount => _lastAreaCollisionCandidateCount;
|
||||
public int LastResolvedAreaHitCount => _lastResolvedAreaHitCount;
|
||||
public float LastCollisionCellSize => _lastCollisionCellSize;
|
||||
public bool LastCollisionHasEnemyTargets => _lastCollisionHasEnemyTargets;
|
||||
|
||||
private void ResetCollisionRuntimeStats()
|
||||
{
|
||||
_lastCollisionQueryCount = 0;
|
||||
_lastProjectileCollisionQueryCount = 0;
|
||||
_lastAreaCollisionQueryCount = 0;
|
||||
_lastCollisionCandidateCount = 0;
|
||||
_lastProjectileCollisionCandidateCount = 0;
|
||||
_lastAreaCollisionCandidateCount = 0;
|
||||
_lastResolvedAreaHitCount = 0;
|
||||
_lastCollisionCellSize = 0f;
|
||||
_lastCollisionHasEnemyTargets = false;
|
||||
}
|
||||
|
||||
private void PrepareCollisionQueryAndCandidateChannels(int queryCount, int expectedCandidateCount,
|
||||
int bucketCapacity)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _collisionQueryInputs, queryCount);
|
||||
EnsureCapacity(ref _collisionCandidates, expectedCandidateCount);
|
||||
EnsureCapacity(ref _enemyCollisionBuckets, bucketCapacity);
|
||||
|
||||
_collisionQueryInputs.Clear();
|
||||
_collisionCandidates.Clear();
|
||||
_enemyCollisionBuckets.Clear();
|
||||
}
|
||||
|
||||
private void AddProjectileCollisionQuery(int queryId, in ProjectileJobOutputData projectile, float radius,
|
||||
int maxTargets = 1)
|
||||
{
|
||||
if (!_collisionQueryInputs.IsCreated || radius <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_collisionQueryInputs.Add(new CollisionQueryData
|
||||
{
|
||||
QueryId = queryId,
|
||||
SourceType = CollisionSourceTypeProjectile,
|
||||
SourceEntityId = projectile.EntityId,
|
||||
SourceOwnerEntityId = projectile.OwnerEntityId,
|
||||
SourceWasActiveAtQueryTime = true,
|
||||
Position = projectile.Position,
|
||||
Radius = radius,
|
||||
MaxTargets = math.max(1, maxTargets),
|
||||
ShapeType = CollisionShapeCircle,
|
||||
Direction = new float3(0f, 0f, 1f),
|
||||
HalfAngleDeg = 180f,
|
||||
HalfWidth = 0f,
|
||||
HalfLength = 0f
|
||||
});
|
||||
}
|
||||
|
||||
private void AddAreaCollisionQuery(int queryId, int sourceEntityId, int sourceOwnerEntityId,
|
||||
bool sourceWasActiveAtQueryTime, in Vector3 center, float radius, int maxTargets, int shapeType,
|
||||
in Vector3 direction, float halfAngleDeg, float halfWidth, float halfLength)
|
||||
{
|
||||
if (!_collisionQueryInputs.IsCreated || radius <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 normalizedDirection = direction;
|
||||
normalizedDirection.y = 0f;
|
||||
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
normalizedDirection = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
normalizedDirection.Normalize();
|
||||
}
|
||||
|
||||
_collisionQueryInputs.Add(new CollisionQueryData
|
||||
{
|
||||
QueryId = queryId,
|
||||
SourceType = CollisionSourceTypeArea,
|
||||
SourceEntityId = sourceEntityId,
|
||||
SourceOwnerEntityId = sourceOwnerEntityId,
|
||||
SourceWasActiveAtQueryTime = sourceWasActiveAtQueryTime,
|
||||
Position = new float3(center.x, center.y, center.z),
|
||||
Radius = radius,
|
||||
MaxTargets = math.max(1, maxTargets),
|
||||
ShapeType = shapeType,
|
||||
Direction = new float3(normalizedDirection.x, normalizedDirection.y, normalizedDirection.z),
|
||||
HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f),
|
||||
HalfWidth = Mathf.Max(0f, halfWidth),
|
||||
HalfLength = Mathf.Max(0f, halfLength)
|
||||
});
|
||||
}
|
||||
|
||||
private void AddCollisionCandidate(int queryId, int sourceType, int sourceEntityId, int sourceOwnerEntityId,
|
||||
int targetEntityId, float sqrDistance)
|
||||
{
|
||||
if (!_collisionCandidates.IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_collisionCandidates.Add(new CollisionCandidateData
|
||||
{
|
||||
QueryId = queryId,
|
||||
SourceType = sourceType,
|
||||
SourceEntityId = sourceEntityId,
|
||||
SourceOwnerEntityId = sourceOwnerEntityId,
|
||||
TargetEntityId = targetEntityId,
|
||||
SqrDistance = sqrDistance
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b0eaf024db24511988ba3bde25359e7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Enemy Separation Temporal
|
||||
|
||||
private void PrepareEnemySeparationJobBuffers(int enemyCount, int bucketCapacity)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _enemyJobSeparationOutputs, enemyCount);
|
||||
_enemyJobSeparationOutputs.Clear();
|
||||
if (enemyCount > 0)
|
||||
{
|
||||
_enemyJobSeparationOutputs.ResizeUninitialized(enemyCount);
|
||||
}
|
||||
|
||||
EnsureCapacity(ref _enemySeparationBuckets, bucketCapacity);
|
||||
_enemySeparationBuckets.Clear();
|
||||
|
||||
EnsureCapacity(ref _enemySeparationPreviousPushes, enemyCount);
|
||||
EnsureCapacity(ref _enemySeparationCurrentPushes, enemyCount);
|
||||
|
||||
if (_enemySeparationPreviousPushes.Length < enemyCount)
|
||||
{
|
||||
int oldLength = _enemySeparationPreviousPushes.Length;
|
||||
_enemySeparationPreviousPushes.ResizeUninitialized(enemyCount);
|
||||
for (int i = oldLength; i < enemyCount; i++)
|
||||
{
|
||||
_enemySeparationPreviousPushes[i] = float2.zero;
|
||||
}
|
||||
}
|
||||
else if (_enemySeparationPreviousPushes.Length > enemyCount)
|
||||
{
|
||||
_enemySeparationPreviousPushes.ResizeUninitialized(enemyCount);
|
||||
}
|
||||
|
||||
_enemySeparationCurrentPushes.Clear();
|
||||
if (enemyCount > 0)
|
||||
{
|
||||
_enemySeparationCurrentPushes.ResizeUninitialized(enemyCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void CommitEnemySeparationTemporalBuffers(int enemyCount)
|
||||
{
|
||||
if (!_enemySeparationPreviousPushes.IsCreated || !_enemySeparationCurrentPushes.IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int copyCount = math.min(enemyCount,
|
||||
math.min(_enemySeparationPreviousPushes.Length, _enemySeparationCurrentPushes.Length));
|
||||
for (int i = 0; i < copyCount; i++)
|
||||
{
|
||||
_enemySeparationPreviousPushes[i] = _enemySeparationCurrentPushes[i];
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnemyAddedToSeparationTemporalBuffers()
|
||||
{
|
||||
if (_enemySeparationPreviousPushes.IsCreated)
|
||||
{
|
||||
_enemySeparationPreviousPushes.Add(float2.zero);
|
||||
}
|
||||
|
||||
if (_enemySeparationCurrentPushes.IsCreated)
|
||||
{
|
||||
_enemySeparationCurrentPushes.Add(float2.zero);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnemyRemovedFromSeparationTemporalBuffers(int removedIndex)
|
||||
{
|
||||
if (_enemySeparationPreviousPushes.IsCreated && removedIndex >= 0 &&
|
||||
removedIndex < _enemySeparationPreviousPushes.Length)
|
||||
{
|
||||
_enemySeparationPreviousPushes.RemoveAtSwapBack(removedIndex);
|
||||
}
|
||||
|
||||
if (_enemySeparationCurrentPushes.IsCreated && removedIndex >= 0 &&
|
||||
removedIndex < _enemySeparationCurrentPushes.Length)
|
||||
{
|
||||
_enemySeparationCurrentPushes.RemoveAtSwapBack(removedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2c8675358384b21925e6f660ce61a79
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
// Shared channel constants plus compatibility fields still reflected by tests.
|
||||
private const int CollisionSourceTypeProjectile = 1;
|
||||
private const int CollisionSourceTypeArea = 2;
|
||||
private const int CollisionShapeCircle = 0;
|
||||
private const int CollisionShapeSector = 1;
|
||||
private const int CollisionShapeRectangle = 2;
|
||||
|
||||
// Kept as top-level fields because current regression tests reflect them directly.
|
||||
private NativeList<CollisionQueryData> _collisionQueryInputs;
|
||||
private readonly List<AreaCollisionRequestData> _areaCollisionRequests = new(16);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20926de0e6e14c7b818779593f1f87bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Job Data Conversion
|
||||
|
||||
private void SyncSimulationStateToJobInputs()
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _enemyJobInputs, _enemies.Count);
|
||||
EnsureCapacity(ref _projectileJobInputs, _projectiles.Count);
|
||||
|
||||
_enemyJobInputs.Clear();
|
||||
_projectileJobInputs.Clear();
|
||||
|
||||
foreach (EnemySimData data in _enemies)
|
||||
{
|
||||
_enemyJobInputs.Add(ConvertToEnemyJobInput(data));
|
||||
}
|
||||
|
||||
foreach (ProjectileSimData data in _projectiles)
|
||||
{
|
||||
_projectileJobInputs.Add(ConvertToProjectileJobInput(data));
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareEnemyJobOutputBuffer(int enemyCount)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _enemyJobOutputs, enemyCount);
|
||||
_enemyJobOutputs.Clear();
|
||||
|
||||
if (enemyCount > 0)
|
||||
{
|
||||
_enemyJobOutputs.ResizeUninitialized(enemyCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareProjectileJobOutputBuffer(int projectileCount)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _projectileJobOutputs, projectileCount);
|
||||
_projectileJobOutputs.Clear();
|
||||
|
||||
if (projectileCount > 0)
|
||||
{
|
||||
_projectileJobOutputs.ResizeUninitialized(projectileCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyProjectileInputsToOutputs()
|
||||
{
|
||||
for (int i = 0; i < _projectileJobInputs.Length; i++)
|
||||
{
|
||||
ProjectileJobInputData input = _projectileJobInputs[i];
|
||||
_projectileJobOutputs[i] = new ProjectileJobOutputData
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
OwnerEntityId = input.OwnerEntityId,
|
||||
Position = input.Position,
|
||||
Forward = input.Forward,
|
||||
Velocity = input.Velocity,
|
||||
Speed = input.Speed,
|
||||
LifeTime = input.LifeTime,
|
||||
Age = input.Age,
|
||||
Active = input.Active,
|
||||
RemainingLifetime = input.RemainingLifetime,
|
||||
State = input.State
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static EnemyJobInputData ConvertToEnemyJobInput(in EnemySimData enemy)
|
||||
{
|
||||
return new EnemyJobInputData
|
||||
{
|
||||
EntityId = enemy.EntityId,
|
||||
Position = new float3(enemy.Position.x, enemy.Position.y, enemy.Position.z),
|
||||
Forward = new float3(enemy.Forward.x, enemy.Forward.y, enemy.Forward.z),
|
||||
Rotation = new quaternion(enemy.Rotation.x, enemy.Rotation.y, enemy.Rotation.z, enemy.Rotation.w),
|
||||
Speed = enemy.Speed,
|
||||
AttackRange = enemy.AttackRange,
|
||||
AvoidEnemyOverlap = enemy.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = enemy.EnemyBodyRadius,
|
||||
SeparationIterations = enemy.SeparationIterations,
|
||||
TargetType = enemy.TargetType,
|
||||
State = enemy.State
|
||||
};
|
||||
}
|
||||
|
||||
private static EnemySimData ConvertToEnemySimData(in EnemyJobOutputData enemy)
|
||||
{
|
||||
return new EnemySimData
|
||||
{
|
||||
EntityId = enemy.EntityId,
|
||||
Position = new Vector3(enemy.Position.x, enemy.Position.y, enemy.Position.z),
|
||||
Forward = new Vector3(enemy.Forward.x, enemy.Forward.y, enemy.Forward.z),
|
||||
Rotation = new Quaternion(enemy.Rotation.value.x, enemy.Rotation.value.y, enemy.Rotation.value.z,
|
||||
enemy.Rotation.value.w),
|
||||
Speed = enemy.Speed,
|
||||
AttackRange = enemy.AttackRange,
|
||||
AvoidEnemyOverlap = enemy.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = enemy.EnemyBodyRadius,
|
||||
SeparationIterations = enemy.SeparationIterations,
|
||||
TargetType = enemy.TargetType,
|
||||
State = enemy.State
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileJobInputData ConvertToProjectileJobInput(in ProjectileSimData projectile)
|
||||
{
|
||||
return new ProjectileJobInputData
|
||||
{
|
||||
EntityId = projectile.EntityId,
|
||||
OwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = new float3(projectile.Position.x, projectile.Position.y, projectile.Position.z),
|
||||
Forward = new float3(projectile.Forward.x, projectile.Forward.y, projectile.Forward.z),
|
||||
Velocity = new float3(projectile.Velocity.x, projectile.Velocity.y, projectile.Velocity.z),
|
||||
Speed = projectile.Speed,
|
||||
LifeTime = projectile.LifeTime,
|
||||
Age = projectile.Age,
|
||||
Active = projectile.Active,
|
||||
RemainingLifetime = projectile.RemainingLifetime,
|
||||
State = projectile.State
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileSimData ConvertToProjectileSimData(in ProjectileJobOutputData projectile)
|
||||
{
|
||||
return new ProjectileSimData
|
||||
{
|
||||
EntityId = projectile.EntityId,
|
||||
OwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = new Vector3(projectile.Position.x, projectile.Position.y, projectile.Position.z),
|
||||
Forward = new Vector3(projectile.Forward.x, projectile.Forward.y, projectile.Forward.z),
|
||||
Velocity = new Vector3(projectile.Velocity.x, projectile.Velocity.y, projectile.Velocity.z),
|
||||
Speed = projectile.Speed,
|
||||
LifeTime = projectile.LifeTime,
|
||||
Age = projectile.Age,
|
||||
Active = projectile.Active,
|
||||
RemainingLifetime = projectile.RemainingLifetime,
|
||||
State = projectile.State
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57a3f582d9c04803908205e0cd4b5b75
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
using System;
|
||||
using Unity.Collections;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Job Data Lifecycle
|
||||
|
||||
private void InitializeJobDataChannels()
|
||||
{
|
||||
if (AreJobDataChannelsUsable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisposeJobDataChannels();
|
||||
_enemyJobInputs = new NativeList<EnemyJobInputData>(64, Allocator.Persistent);
|
||||
_enemyJobOutputs = new NativeList<EnemyJobOutputData>(64, Allocator.Persistent);
|
||||
_enemyJobSeparationOutputs = new NativeList<EnemyJobOutputData>(64, Allocator.Persistent);
|
||||
_enemySeparationPreviousPushes = new NativeList<float2>(64, Allocator.Persistent);
|
||||
_enemySeparationCurrentPushes = new NativeList<float2>(64, Allocator.Persistent);
|
||||
_projectileJobInputs = new NativeList<ProjectileJobInputData>(64, Allocator.Persistent);
|
||||
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
|
||||
_collisionQueryInputs = new NativeList<CollisionQueryData>(64, Allocator.Persistent);
|
||||
_collisionCandidates = new NativeList<CollisionCandidateData>(128, Allocator.Persistent);
|
||||
_enemySeparationBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
|
||||
_enemyCollisionBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
|
||||
InitializeEnemyTargetSpatialIndex();
|
||||
}
|
||||
|
||||
private void DisposeJobDataChannels()
|
||||
{
|
||||
if (_enemyJobInputs.IsCreated)
|
||||
{
|
||||
_enemyJobInputs.Dispose();
|
||||
}
|
||||
|
||||
_enemyJobInputs = default;
|
||||
|
||||
if (_enemyJobOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobOutputs.Dispose();
|
||||
}
|
||||
|
||||
_enemyJobOutputs = default;
|
||||
|
||||
if (_enemyJobSeparationOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobSeparationOutputs.Dispose();
|
||||
}
|
||||
|
||||
_enemyJobSeparationOutputs = default;
|
||||
|
||||
if (_enemySeparationPreviousPushes.IsCreated)
|
||||
{
|
||||
_enemySeparationPreviousPushes.Dispose();
|
||||
}
|
||||
|
||||
_enemySeparationPreviousPushes = default;
|
||||
|
||||
if (_enemySeparationCurrentPushes.IsCreated)
|
||||
{
|
||||
_enemySeparationCurrentPushes.Dispose();
|
||||
}
|
||||
|
||||
_enemySeparationCurrentPushes = default;
|
||||
|
||||
if (_projectileJobInputs.IsCreated)
|
||||
{
|
||||
_projectileJobInputs.Dispose();
|
||||
}
|
||||
|
||||
_projectileJobInputs = default;
|
||||
|
||||
if (_projectileJobOutputs.IsCreated)
|
||||
{
|
||||
_projectileJobOutputs.Dispose();
|
||||
}
|
||||
|
||||
_projectileJobOutputs = default;
|
||||
|
||||
if (_collisionQueryInputs.IsCreated)
|
||||
{
|
||||
_collisionQueryInputs.Dispose();
|
||||
}
|
||||
|
||||
_collisionQueryInputs = default;
|
||||
|
||||
if (_collisionCandidates.IsCreated)
|
||||
{
|
||||
_collisionCandidates.Dispose();
|
||||
}
|
||||
|
||||
_collisionCandidates = default;
|
||||
|
||||
if (_enemySeparationBuckets.IsCreated)
|
||||
{
|
||||
_enemySeparationBuckets.Dispose();
|
||||
}
|
||||
|
||||
_enemySeparationBuckets = default;
|
||||
|
||||
if (_enemyCollisionBuckets.IsCreated)
|
||||
{
|
||||
_enemyCollisionBuckets.Dispose();
|
||||
}
|
||||
|
||||
_enemyCollisionBuckets = default;
|
||||
|
||||
DisposeEnemyTargetSpatialIndex();
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
ResetCollisionRuntimeStats();
|
||||
}
|
||||
|
||||
private void ClearJobDataChannels()
|
||||
{
|
||||
if (!AreJobDataChannelsUsable())
|
||||
{
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
ResetCollisionRuntimeStats();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_enemyJobInputs.IsCreated)
|
||||
{
|
||||
_enemyJobInputs.Clear();
|
||||
}
|
||||
|
||||
if (_enemyJobOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobOutputs.Clear();
|
||||
}
|
||||
|
||||
if (_projectileJobInputs.IsCreated)
|
||||
{
|
||||
_projectileJobInputs.Clear();
|
||||
}
|
||||
|
||||
if (_projectileJobOutputs.IsCreated)
|
||||
{
|
||||
_projectileJobOutputs.Clear();
|
||||
}
|
||||
|
||||
if (_collisionQueryInputs.IsCreated)
|
||||
{
|
||||
_collisionQueryInputs.Clear();
|
||||
}
|
||||
|
||||
if (_collisionCandidates.IsCreated)
|
||||
{
|
||||
_collisionCandidates.Clear();
|
||||
}
|
||||
|
||||
if (_enemyJobSeparationOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobSeparationOutputs.Clear();
|
||||
}
|
||||
|
||||
if (_enemySeparationPreviousPushes.IsCreated)
|
||||
{
|
||||
_enemySeparationPreviousPushes.Clear();
|
||||
}
|
||||
|
||||
if (_enemySeparationCurrentPushes.IsCreated)
|
||||
{
|
||||
_enemySeparationCurrentPushes.Clear();
|
||||
}
|
||||
|
||||
if (_enemySeparationBuckets.IsCreated)
|
||||
{
|
||||
_enemySeparationBuckets.Clear();
|
||||
}
|
||||
|
||||
if (_enemyCollisionBuckets.IsCreated)
|
||||
{
|
||||
_enemyCollisionBuckets.Clear();
|
||||
}
|
||||
|
||||
ClearEnemyTargetSpatialIndex();
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
ResetCollisionRuntimeStats();
|
||||
}
|
||||
|
||||
private bool AreJobDataChannelsUsable()
|
||||
{
|
||||
return IsNativeListUsable(_enemyJobInputs) &&
|
||||
IsNativeListUsable(_enemyJobOutputs) &&
|
||||
IsNativeListUsable(_enemyJobSeparationOutputs) &&
|
||||
IsNativeListUsable(_enemySeparationPreviousPushes) &&
|
||||
IsNativeListUsable(_enemySeparationCurrentPushes) &&
|
||||
IsNativeListUsable(_projectileJobInputs) &&
|
||||
IsNativeListUsable(_projectileJobOutputs) &&
|
||||
IsNativeListUsable(_collisionQueryInputs) &&
|
||||
IsNativeListUsable(_collisionCandidates) &&
|
||||
IsNativeMultiHashMapUsable(_enemySeparationBuckets) &&
|
||||
IsNativeMultiHashMapUsable(_enemyCollisionBuckets);
|
||||
}
|
||||
|
||||
private static void EnsureCapacity<T>(ref NativeList<T> nativeList, int targetCount) where T : unmanaged
|
||||
{
|
||||
if (!nativeList.IsCreated || targetCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (nativeList.Capacity < targetCount)
|
||||
{
|
||||
nativeList.Capacity = targetCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureCapacity(ref NativeParallelMultiHashMap<long, int> hashMap, int targetCount)
|
||||
{
|
||||
if (!hashMap.IsCreated || targetCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hashMap.Capacity < targetCount)
|
||||
{
|
||||
hashMap.Capacity = targetCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNativeListUsable<T>(NativeList<T> nativeList) where T : unmanaged
|
||||
{
|
||||
if (!nativeList.IsCreated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = nativeList.Length;
|
||||
return true;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNativeMultiHashMapUsable(NativeParallelMultiHashMap<long, int> hashMap)
|
||||
{
|
||||
if (!hashMap.IsCreated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = hashMap.Count();
|
||||
return true;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2e46cc42cdd45f5badd02bd4635ba97
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Job Output Commit
|
||||
|
||||
private void ApplyJobOutputsToSimulationState()
|
||||
{
|
||||
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
|
||||
bool hasEnemyPositionChanged = false;
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
EnemyJobOutputData output = _enemyJobOutputs[i];
|
||||
if (!hasEnemyPositionChanged)
|
||||
{
|
||||
Vector3 currentPosition = _enemies[i].Position;
|
||||
hasEnemyPositionChanged = currentPosition.x != output.Position.x ||
|
||||
currentPosition.z != output.Position.z;
|
||||
}
|
||||
|
||||
_enemies[i] = ConvertToEnemySimData(output);
|
||||
}
|
||||
|
||||
if (hasEnemyPositionChanged)
|
||||
{
|
||||
MarkEnemyTargetSpatialIndexDirty();
|
||||
}
|
||||
|
||||
int projectileCount = Mathf.Min(_projectiles.Count, _projectileJobOutputs.Length);
|
||||
for (int i = 0; i < projectileCount; i++)
|
||||
{
|
||||
_projectiles[i] = ConvertToProjectileSimData(_projectileJobOutputs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59ae2375305641a6a36d1bea0bf1abc2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed class EntityBinding
|
||||
{
|
||||
private readonly Dictionary<int, int> _entityIdToSimulationIndex = new Dictionary<int, int>();
|
||||
private readonly Dictionary<int, int> _simulationIndexToEntityId = new Dictionary<int, int>();
|
||||
|
||||
public int Count => _entityIdToSimulationIndex.Count;
|
||||
|
||||
public void Bind(int entityId, int simulationIndex)
|
||||
{
|
||||
if (_entityIdToSimulationIndex.TryGetValue(entityId, out int oldSimulationIndex))
|
||||
{
|
||||
_simulationIndexToEntityId.Remove(oldSimulationIndex);
|
||||
}
|
||||
|
||||
if (_simulationIndexToEntityId.TryGetValue(simulationIndex, out int oldEntityId))
|
||||
{
|
||||
_entityIdToSimulationIndex.Remove(oldEntityId);
|
||||
}
|
||||
|
||||
_entityIdToSimulationIndex[entityId] = simulationIndex;
|
||||
_simulationIndexToEntityId[simulationIndex] = entityId;
|
||||
}
|
||||
|
||||
public void RemapIndex(int entityId, int newSimulationIndex)
|
||||
{
|
||||
if (!_entityIdToSimulationIndex.TryGetValue(entityId, out int oldSimulationIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_simulationIndexToEntityId.TryGetValue(newSimulationIndex, out int oldEntityId) &&
|
||||
oldEntityId != entityId)
|
||||
{
|
||||
_entityIdToSimulationIndex.Remove(oldEntityId);
|
||||
}
|
||||
|
||||
_simulationIndexToEntityId.Remove(oldSimulationIndex);
|
||||
_entityIdToSimulationIndex[entityId] = newSimulationIndex;
|
||||
_simulationIndexToEntityId[newSimulationIndex] = entityId;
|
||||
}
|
||||
|
||||
public bool TryGetSimulationIndex(int entityId, out int simulationIndex)
|
||||
{
|
||||
return _entityIdToSimulationIndex.TryGetValue(entityId, out simulationIndex);
|
||||
}
|
||||
|
||||
public bool TryGetEntityId(int simulationIndex, out int entityId)
|
||||
{
|
||||
return _simulationIndexToEntityId.TryGetValue(simulationIndex, out entityId);
|
||||
}
|
||||
|
||||
public bool UnbindByEntityId(int entityId)
|
||||
{
|
||||
if (!_entityIdToSimulationIndex.TryGetValue(entityId, out int simulationIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_entityIdToSimulationIndex.Remove(entityId);
|
||||
_simulationIndexToEntityId.Remove(simulationIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool UnbindBySimulationIndex(int simulationIndex)
|
||||
{
|
||||
if (!_simulationIndexToEntityId.TryGetValue(simulationIndex, out int entityId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_simulationIndexToEntityId.Remove(simulationIndex);
|
||||
_entityIdToSimulationIndex.Remove(entityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_entityIdToSimulationIndex.Clear();
|
||||
_simulationIndexToEntityId.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4f6d93561db4b9092ab61182f2983d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f549ca9decb4f38933329abacbf78ac
|
||||
timeCreated: 1771901120
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct AreaCollisionHitEventData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public int TargetEntityId;
|
||||
public float SqrDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5140d989cba042e6bcb217893491273d
|
||||
timeCreated: 1771901785
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct AreaCollisionRequestData
|
||||
{
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public bool SourceWasActiveAtQueryTime;
|
||||
public Vector3 Center;
|
||||
public float Radius;
|
||||
public int MaxTargets;
|
||||
public int ShapeType;
|
||||
public Vector3 Direction;
|
||||
public float HalfAngleDeg;
|
||||
public float HalfWidth;
|
||||
public float HalfLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e32f4e0724314c70882f5ed043a4dca4
|
||||
timeCreated: 1771901749
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[BurstCompile]
|
||||
private struct BuildEnemySeparationBucketsBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobOutputData> Inputs;
|
||||
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
|
||||
public float CellSize;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
BuildEnemySeparationBucket(
|
||||
index,
|
||||
Inputs,
|
||||
Buckets,
|
||||
CellSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildEnemySeparationBucket(
|
||||
int index,
|
||||
NativeArray<EnemyJobOutputData> inputs,
|
||||
NativeParallelMultiHashMap<long, int>.ParallelWriter buckets,
|
||||
float cellSize
|
||||
)
|
||||
{
|
||||
EnemyJobOutputData output = inputs[index];
|
||||
if (!output.AvoidEnemyOverlap)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float3 position = output.Position;
|
||||
position.y = 0f;
|
||||
int cellX = (int)math.floor(position.x / cellSize);
|
||||
int cellZ = (int)math.floor(position.z / cellSize);
|
||||
buckets.Add(SeparationCellKey(cellX, cellZ), index);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e833e91677bb49a4a458c9c8b90099e9
|
||||
timeCreated: 1771901500
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct CollisionCandidateData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceType;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public int TargetEntityId;
|
||||
public float SqrDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 453c14964a4b4aed89c0abd7b25cd26c
|
||||
timeCreated: 1771901722
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct CollisionQueryData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceType;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public bool SourceWasActiveAtQueryTime;
|
||||
public float3 Position;
|
||||
public float Radius;
|
||||
public int MaxTargets;
|
||||
public int ShapeType;
|
||||
public float3 Direction;
|
||||
public float HalfAngleDeg;
|
||||
public float HalfWidth;
|
||||
public float HalfLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58abe0d4fd7a49acb716fbd26e0fb19f
|
||||
timeCreated: 1771901691
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct EnemyJobInputData
|
||||
{
|
||||
public int EntityId;
|
||||
public float3 Position;
|
||||
public float3 Forward;
|
||||
public quaternion Rotation;
|
||||
public float Speed;
|
||||
public float AttackRange;
|
||||
public bool AvoidEnemyOverlap;
|
||||
public float EnemyBodyRadius;
|
||||
public int SeparationIterations;
|
||||
public int TargetType;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 171936b8a6b84a8fbacc7651ce473b98
|
||||
timeCreated: 1771901146
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct EnemyJobOutputData
|
||||
{
|
||||
public int EntityId;
|
||||
public float3 Position;
|
||||
public float3 Forward;
|
||||
public quaternion Rotation;
|
||||
public float Speed;
|
||||
public float AttackRange;
|
||||
public bool AvoidEnemyOverlap;
|
||||
public float EnemyBodyRadius;
|
||||
public int SeparationIterations;
|
||||
public int TargetType;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34ef6fbf123f45ca88d8e7ab8311d543
|
||||
timeCreated: 1771901204
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[BurstCompile]
|
||||
private struct EnemyMovementBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobInputData> Inputs;
|
||||
public NativeArray<EnemyJobOutputData> Outputs;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteEnemyMovement(
|
||||
index,
|
||||
Inputs,
|
||||
Outputs,
|
||||
DeltaTime,
|
||||
PlayerPosition
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteEnemyMovement(
|
||||
int index,
|
||||
NativeArray<EnemyJobInputData> inputs,
|
||||
NativeArray<EnemyJobOutputData> outputs,
|
||||
float deltaTime,
|
||||
float3 playerPosition
|
||||
)
|
||||
{
|
||||
EnemyJobInputData input = inputs[index];
|
||||
float attackRange = input.AttackRange > 0f ? input.AttackRange : DefaultAttackRange;
|
||||
float attackRangeSqr = attackRange * attackRange;
|
||||
|
||||
float3 currentPosition = input.Position;
|
||||
float3 horizontalPosition = new float3(currentPosition.x, 0f, currentPosition.z);
|
||||
float3 toPlayer = playerPosition - horizontalPosition;
|
||||
float sqrDistance = math.lengthsq(toPlayer);
|
||||
bool isInAttackRange = sqrDistance <= attackRangeSqr;
|
||||
bool canChase = !isInAttackRange && input.Speed > 0f && sqrDistance > float.Epsilon;
|
||||
|
||||
float3 forward = input.Forward;
|
||||
float3 desiredPosition = currentPosition;
|
||||
quaternion rotation = input.Rotation;
|
||||
|
||||
if (canChase)
|
||||
{
|
||||
forward = math.normalizesafe(toPlayer, forward);
|
||||
desiredPosition = currentPosition + forward * input.Speed * deltaTime;
|
||||
if (math.lengthsq(forward) > float.Epsilon)
|
||||
{
|
||||
rotation = quaternion.LookRotationSafe(forward, math.up());
|
||||
}
|
||||
}
|
||||
|
||||
int nextState;
|
||||
if (isInAttackRange)
|
||||
{
|
||||
nextState = EnemyStateInAttackRange;
|
||||
}
|
||||
else if (canChase)
|
||||
{
|
||||
nextState = EnemyStateChasing;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextState = EnemyStateIdle;
|
||||
}
|
||||
|
||||
outputs[index] = new EnemyJobOutputData
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
Position = desiredPosition,
|
||||
Forward = forward,
|
||||
Rotation = rotation,
|
||||
Speed = input.Speed,
|
||||
AttackRange = attackRange,
|
||||
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = input.EnemyBodyRadius,
|
||||
SeparationIterations = input.SeparationIterations,
|
||||
TargetType = input.TargetType,
|
||||
State = nextState
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abd5519779ed41d4947280eec326ffb1
|
||||
timeCreated: 1771901472
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[BurstCompile]
|
||||
private struct EnemySeparationBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobOutputData> Inputs;
|
||||
[ReadOnly] public NativeParallelMultiHashMap<long, int> Buckets;
|
||||
[ReadOnly] public NativeArray<float2> PreviousPushes;
|
||||
public NativeArray<EnemyJobOutputData> Outputs;
|
||||
public NativeArray<float2> CurrentPushes;
|
||||
public float CellSize;
|
||||
public float MaxRadius;
|
||||
public float3 PlayerPosition;
|
||||
public float PushDamping;
|
||||
public float MaxStepScale;
|
||||
public bool UseTangentialInAttackRange;
|
||||
public float PushSmoothing;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteEnemySeparation(
|
||||
index,
|
||||
Inputs,
|
||||
Buckets,
|
||||
Outputs,
|
||||
CellSize,
|
||||
MaxRadius,
|
||||
PlayerPosition,
|
||||
PushDamping,
|
||||
MaxStepScale,
|
||||
UseTangentialInAttackRange,
|
||||
PreviousPushes,
|
||||
CurrentPushes,
|
||||
PushSmoothing
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteEnemySeparation(
|
||||
int index,
|
||||
NativeArray<EnemyJobOutputData> inputs,
|
||||
NativeParallelMultiHashMap<long, int> buckets,
|
||||
NativeArray<EnemyJobOutputData> outputs,
|
||||
float cellSize,
|
||||
float maxRadius,
|
||||
float3 playerPosition,
|
||||
float pushDamping,
|
||||
float maxStepScale,
|
||||
bool useTangentialInAttackRange,
|
||||
NativeArray<float2> previousPushes,
|
||||
NativeArray<float2> currentPushes,
|
||||
float pushSmoothing
|
||||
)
|
||||
{
|
||||
currentPushes[index] = float2.zero;
|
||||
EnemyJobOutputData self = inputs[index];
|
||||
if (!self.AvoidEnemyOverlap)
|
||||
{
|
||||
outputs[index] = self;
|
||||
return;
|
||||
}
|
||||
|
||||
float3 candidate = self.Position;
|
||||
candidate.y = 0f;
|
||||
float3 original = candidate;
|
||||
float3 fallback =
|
||||
math.normalizesafe(new float3(self.Forward.x, 0f, self.Forward.z), new float3(1f, 0f, 0f));
|
||||
float selfRadius = self.EnemyBodyRadius > 0f ? self.EnemyBodyRadius : 0.45f;
|
||||
int iterations = self.SeparationIterations > 0 ? self.SeparationIterations : 1;
|
||||
int queryRange = math.max(1, (int)math.ceil((selfRadius + maxRadius) / cellSize));
|
||||
|
||||
for (int iter = 0; iter < iterations; iter++)
|
||||
{
|
||||
int cellX = (int)math.floor(candidate.x / cellSize);
|
||||
int cellZ = (int)math.floor(candidate.z / cellSize);
|
||||
float3 pushAccumulation = float3.zero;
|
||||
|
||||
for (int dx = -queryRange; dx <= queryRange; dx++)
|
||||
{
|
||||
for (int dz = -queryRange; dz <= queryRange; dz++)
|
||||
{
|
||||
long key = SeparationCellKey(cellX + dx, cellZ + dz);
|
||||
if (!buckets.TryGetFirstValue(key, out int otherIndex,
|
||||
out NativeParallelMultiHashMapIterator<long> iterator))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (otherIndex == index)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyJobOutputData other = inputs[otherIndex];
|
||||
if (!other.AvoidEnemyOverlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float otherRadius = other.EnemyBodyRadius > 0f ? other.EnemyBodyRadius : 0.45f;
|
||||
float minDistance = selfRadius + otherRadius;
|
||||
float minDistanceSqr = minDistance * minDistance;
|
||||
|
||||
float3 otherPosition = other.Position;
|
||||
otherPosition.y = 0f;
|
||||
float3 toSelf = candidate - otherPosition;
|
||||
float sqrDistance = math.lengthsq(toSelf);
|
||||
|
||||
if (sqrDistance <= float.Epsilon)
|
||||
{
|
||||
float3 zeroDistanceAxis = GetZeroDistanceSeparationAxis(index, otherIndex);
|
||||
float directionSign = index < otherIndex ? 1f : -1f;
|
||||
pushAccumulation += zeroDistanceAxis * (selfRadius * 0.25f * directionSign);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sqrDistance >= minDistanceSqr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float distance = math.sqrt(sqrDistance);
|
||||
float penetration = minDistance - distance;
|
||||
pushAccumulation += (toSelf / distance) * penetration;
|
||||
} while (buckets.TryGetNextValue(out otherIndex, ref iterator));
|
||||
}
|
||||
}
|
||||
|
||||
if (math.lengthsq(pushAccumulation) <= float.Epsilon)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float3 resolvedPush = pushAccumulation * pushDamping;
|
||||
|
||||
float maxStep = selfRadius * maxStepScale;
|
||||
float pushLength = math.length(resolvedPush);
|
||||
if (pushLength > maxStep && pushLength > float.Epsilon)
|
||||
{
|
||||
resolvedPush = resolvedPush / pushLength * maxStep;
|
||||
}
|
||||
|
||||
candidate += resolvedPush;
|
||||
}
|
||||
|
||||
float3 framePush = candidate - original;
|
||||
float2 previousPush2 = previousPushes[index];
|
||||
float3 previousPush = new float3(previousPush2.x, 0f, previousPush2.y);
|
||||
float3 smoothedPush = SmoothSeparationPush(framePush, previousPush, pushSmoothing);
|
||||
|
||||
if (useTangentialInAttackRange && self.State == EnemyStateInAttackRange)
|
||||
{
|
||||
smoothedPush = ProjectToTangential(smoothedPush, playerPosition, original);
|
||||
}
|
||||
|
||||
float maxTotalStep = selfRadius * maxStepScale * iterations;
|
||||
float smoothedLength = math.length(smoothedPush);
|
||||
if (smoothedLength > maxTotalStep && smoothedLength > float.Epsilon)
|
||||
{
|
||||
smoothedPush = smoothedPush / smoothedLength * maxTotalStep;
|
||||
}
|
||||
|
||||
float3 finalPosition = original + smoothedPush;
|
||||
currentPushes[index] = new float2(smoothedPush.x, smoothedPush.z);
|
||||
self.Position = new float3(finalPosition.x, self.Position.y, finalPosition.z);
|
||||
if (math.lengthsq(smoothedPush) > float.Epsilon)
|
||||
{
|
||||
self.Forward = new float3(fallback.x, self.Forward.y, fallback.z);
|
||||
}
|
||||
|
||||
outputs[index] = self;
|
||||
}
|
||||
|
||||
private static float3 SmoothSeparationPush(float3 framePush, float3 previousPush, float pushSmoothing)
|
||||
{
|
||||
float frameLengthSqr = math.lengthsq(framePush);
|
||||
float previousLengthSqr = math.lengthsq(previousPush);
|
||||
|
||||
if (frameLengthSqr <= float.Epsilon)
|
||||
{
|
||||
return float3.zero;
|
||||
}
|
||||
|
||||
if (previousLengthSqr <= float.Epsilon || pushSmoothing <= 0f)
|
||||
{
|
||||
return framePush;
|
||||
}
|
||||
|
||||
float frameLength = math.sqrt(frameLengthSqr);
|
||||
float previousLength = math.sqrt(previousLengthSqr);
|
||||
float3 frameDirection = framePush / frameLength;
|
||||
float3 previousDirection = previousPush / previousLength;
|
||||
float directionAlignment = math.dot(frameDirection, previousDirection);
|
||||
|
||||
if (directionAlignment >= 0.35f)
|
||||
{
|
||||
return framePush;
|
||||
}
|
||||
|
||||
float directionalFactor = math.saturate((0.35f - directionAlignment) / 1.35f);
|
||||
float smoothingStrength = pushSmoothing * directionalFactor;
|
||||
return math.lerp(framePush, previousPush, smoothingStrength);
|
||||
}
|
||||
|
||||
private static float3 ProjectToTangential(float3 push, float3 playerPosition, float3 currentPosition)
|
||||
{
|
||||
if (math.lengthsq(push) <= float.Epsilon)
|
||||
{
|
||||
return push;
|
||||
}
|
||||
|
||||
float3 toPlayer = playerPosition - currentPosition;
|
||||
float toPlayerSqr = math.lengthsq(toPlayer);
|
||||
if (toPlayerSqr <= float.Epsilon)
|
||||
{
|
||||
return push;
|
||||
}
|
||||
|
||||
float3 radialDirection = toPlayer / math.sqrt(toPlayerSqr);
|
||||
float radialOffset = math.dot(push, radialDirection);
|
||||
return push - radialDirection * radialOffset;
|
||||
}
|
||||
|
||||
private static float3 GetZeroDistanceSeparationAxis(int index, int otherIndex)
|
||||
{
|
||||
int lowIndex = math.min(index, otherIndex);
|
||||
int highIndex = math.max(index, otherIndex);
|
||||
uint pairHash = (uint)(lowIndex * 73856093) ^ (uint)(highIndex * 19349663);
|
||||
|
||||
float axisX = (pairHash & 1023u) / 511.5f - 1f;
|
||||
float axisZ = ((pairHash >> 10) & 1023u) / 511.5f - 1f;
|
||||
float3 axis = new float3(axisX, 0f, axisZ);
|
||||
return math.normalizesafe(axis, new float3(1f, 0f, 0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94a77477c3a8410cb58b5028dd5d2c63
|
||||
timeCreated: 1771901543
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct ProjectileJobInputData
|
||||
{
|
||||
public int EntityId;
|
||||
public int OwnerEntityId;
|
||||
public float3 Position;
|
||||
public float3 Forward;
|
||||
public float3 Velocity;
|
||||
public float Speed;
|
||||
public float LifeTime;
|
||||
public float Age;
|
||||
public bool Active;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 334ca3869d7940058256ba5419573637
|
||||
timeCreated: 1771901235
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct ProjectileJobOutputData
|
||||
{
|
||||
public int EntityId;
|
||||
public int OwnerEntityId;
|
||||
public float3 Position;
|
||||
public float3 Forward;
|
||||
public float3 Velocity;
|
||||
public float Speed;
|
||||
public float LifeTime;
|
||||
public float Age;
|
||||
public bool Active;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5c4a144914548379d9991d8ef061da7
|
||||
timeCreated: 1771901266
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[BurstCompile]
|
||||
private struct ProjectileMovementBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<ProjectileJobInputData> Inputs;
|
||||
public NativeArray<ProjectileJobOutputData> Outputs;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
public float MaxSqrDistanceFromPlayer;
|
||||
public float MaxVerticalOffsetFromPlayer;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteProjectileMovement(
|
||||
index,
|
||||
Inputs,
|
||||
Outputs,
|
||||
DeltaTime,
|
||||
PlayerPosition,
|
||||
MaxSqrDistanceFromPlayer,
|
||||
MaxVerticalOffsetFromPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteProjectileMovement(
|
||||
int index,
|
||||
NativeArray<ProjectileJobInputData> inputs,
|
||||
NativeArray<ProjectileJobOutputData> outputs,
|
||||
float deltaTime,
|
||||
float3 playerPosition,
|
||||
float maxSqrDistanceFromPlayer,
|
||||
float maxVerticalOffsetFromPlayer)
|
||||
{
|
||||
ProjectileJobInputData input = inputs[index];
|
||||
ProjectileJobOutputData output = new ProjectileJobOutputData
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
OwnerEntityId = input.OwnerEntityId,
|
||||
Position = input.Position,
|
||||
Forward = input.Forward,
|
||||
Velocity = input.Velocity,
|
||||
Speed = input.Speed,
|
||||
LifeTime = input.LifeTime,
|
||||
Age = input.Age,
|
||||
Active = input.Active,
|
||||
RemainingLifetime = input.RemainingLifetime,
|
||||
State = input.State
|
||||
};
|
||||
|
||||
if (!input.Active)
|
||||
{
|
||||
output.State = ProjectileStateExpired;
|
||||
outputs[index] = output;
|
||||
return;
|
||||
}
|
||||
|
||||
float3 position = input.Position;
|
||||
float3 forward = input.Forward;
|
||||
float3 velocity = input.Velocity;
|
||||
if (math.lengthsq(velocity) <= float.Epsilon && input.Speed > 0f)
|
||||
{
|
||||
float3 moveDirection = math.normalizesafe(forward, new float3(0f, 0f, 1f));
|
||||
velocity = moveDirection * input.Speed;
|
||||
}
|
||||
|
||||
float3 nextPosition = position + velocity * deltaTime;
|
||||
float nextAge = math.max(0f, input.Age + deltaTime);
|
||||
float nextRemainingLifetime = input.RemainingLifetime;
|
||||
bool shouldExpire = false;
|
||||
|
||||
if (input.LifeTime > 0f)
|
||||
{
|
||||
nextRemainingLifetime = math.max(0f, input.LifeTime - nextAge);
|
||||
shouldExpire = nextAge >= input.LifeTime;
|
||||
}
|
||||
else if (input.RemainingLifetime > 0f)
|
||||
{
|
||||
nextRemainingLifetime = math.max(0f, input.RemainingLifetime - deltaTime);
|
||||
shouldExpire = nextRemainingLifetime <= float.Epsilon;
|
||||
}
|
||||
|
||||
if (!shouldExpire && maxSqrDistanceFromPlayer > 0f)
|
||||
{
|
||||
float3 horizontalDelta =
|
||||
new float3(nextPosition.x - playerPosition.x, 0f, nextPosition.z - playerPosition.z);
|
||||
shouldExpire = math.lengthsq(horizontalDelta) > maxSqrDistanceFromPlayer;
|
||||
}
|
||||
|
||||
if (!shouldExpire && maxVerticalOffsetFromPlayer > 0f)
|
||||
{
|
||||
shouldExpire = math.abs(nextPosition.y - playerPosition.y) > maxVerticalOffsetFromPlayer;
|
||||
}
|
||||
|
||||
output.Position = nextPosition;
|
||||
output.Velocity = velocity;
|
||||
output.Age = nextAge;
|
||||
output.RemainingLifetime = nextRemainingLifetime;
|
||||
output.Active = !shouldExpire;
|
||||
output.State = shouldExpire ? ProjectileStateExpired : ProjectileStateActive;
|
||||
|
||||
if (math.lengthsq(velocity) > float.Epsilon)
|
||||
{
|
||||
float3 moveForward = math.normalizesafe(velocity, forward);
|
||||
output.Forward = moveForward;
|
||||
}
|
||||
|
||||
outputs[index] = output;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49001e849c1b4afdaa37d851b9b76866
|
||||
timeCreated: 1771901884
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[BurstCompile]
|
||||
private struct QueryCollisionCandidatesBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<CollisionQueryData> Queries;
|
||||
[ReadOnly] public NativeParallelMultiHashMap<long, int> EnemyBuckets;
|
||||
[ReadOnly] public NativeArray<EnemyJobOutputData> EnemyOutputs;
|
||||
public NativeList<CollisionCandidateData>.ParallelWriter Candidates;
|
||||
public bool HasEnemyTargets;
|
||||
public bool HasPlayerTarget;
|
||||
public int PlayerTargetEntityId;
|
||||
public float3 PlayerPosition;
|
||||
public float CellSize;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
CollisionQueryData query = Queries[index];
|
||||
int centerCellX = (int)math.floor(query.Position.x / CellSize);
|
||||
int centerCellZ = (int)math.floor(query.Position.z / CellSize);
|
||||
int queryRange = math.max(1, (int)math.ceil(query.Radius / CellSize));
|
||||
int selectedCount = 0;
|
||||
bool reachedLimit = false;
|
||||
|
||||
if (HasPlayerTarget && query.SourceEntityId != PlayerTargetEntityId &&
|
||||
query.SourceOwnerEntityId != PlayerTargetEntityId)
|
||||
{
|
||||
float3 playerPosition = PlayerPosition;
|
||||
if (query.SourceType == CollisionSourceTypeArea)
|
||||
{
|
||||
playerPosition.y = query.Position.y;
|
||||
}
|
||||
float3 playerDelta = playerPosition - query.Position;
|
||||
float playerSqrDistance = math.lengthsq(playerDelta);
|
||||
float playerRadiusSqr = query.Radius * query.Radius;
|
||||
if (playerSqrDistance <= playerRadiusSqr)
|
||||
{
|
||||
Candidates.AddNoResize(new CollisionCandidateData
|
||||
{
|
||||
QueryId = query.QueryId,
|
||||
SourceType = query.SourceType,
|
||||
SourceEntityId = query.SourceEntityId,
|
||||
SourceOwnerEntityId = query.SourceOwnerEntityId,
|
||||
TargetEntityId = PlayerTargetEntityId,
|
||||
SqrDistance = playerSqrDistance
|
||||
});
|
||||
|
||||
selectedCount++;
|
||||
if (selectedCount >= query.MaxTargets)
|
||||
{
|
||||
reachedLimit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!HasEnemyTargets || reachedLimit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int dx = -queryRange; dx <= queryRange && !reachedLimit; dx++)
|
||||
{
|
||||
for (int dz = -queryRange; dz <= queryRange && !reachedLimit; dz++)
|
||||
{
|
||||
long key = SeparationCellKey(centerCellX + dx, centerCellZ + dz);
|
||||
if (!EnemyBuckets.TryGetFirstValue(key, out int enemyIndex,
|
||||
out NativeParallelMultiHashMapIterator<long> iterator))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (enemyIndex < 0 || enemyIndex >= EnemyOutputs.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyJobOutputData enemy = EnemyOutputs[enemyIndex];
|
||||
if (enemy.EntityId == query.SourceOwnerEntityId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float deltaY = query.SourceType == CollisionSourceTypeArea
|
||||
? 0f
|
||||
: enemy.Position.y - query.Position.y;
|
||||
float3 delta = new float3(
|
||||
enemy.Position.x - query.Position.x,
|
||||
deltaY,
|
||||
enemy.Position.z - query.Position.z);
|
||||
float sqrDistance = math.lengthsq(delta);
|
||||
float targetRadius = query.SourceType == CollisionSourceTypeArea
|
||||
? math.max(0f, enemy.EnemyBodyRadius)
|
||||
: 0f;
|
||||
float effectiveRadius = query.Radius + targetRadius;
|
||||
if (sqrDistance > effectiveRadius * effectiveRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Candidates.AddNoResize(new CollisionCandidateData
|
||||
{
|
||||
QueryId = query.QueryId,
|
||||
SourceType = query.SourceType,
|
||||
SourceEntityId = query.SourceEntityId,
|
||||
SourceOwnerEntityId = query.SourceOwnerEntityId,
|
||||
TargetEntityId = enemy.EntityId,
|
||||
SqrDistance = sqrDistance
|
||||
});
|
||||
|
||||
selectedCount++;
|
||||
|
||||
if (selectedCount >= query.MaxTargets)
|
||||
{
|
||||
reachedLimit = true;
|
||||
break;
|
||||
}
|
||||
} while (EnemyBuckets.TryGetNextValue(out enemyIndex, ref iterator));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81aa43bb35f547c48b867eafa17a3857
|
||||
timeCreated: 1771914779
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c66a8c10fffb404c9f4819adffea45fd
|
||||
timeCreated: 1771900893
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
using CustomDebugger;
|
||||
using Entity;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Collision Broad Phase
|
||||
|
||||
private void PrepareCollisionCandidatesForFrame()
|
||||
{
|
||||
_collisionCandidateQueryScheduled = false;
|
||||
|
||||
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated ||
|
||||
!_enemyCollisionBuckets.IsCreated)
|
||||
{
|
||||
ResetCollisionRuntimeStats();
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
int projectileCount = _projectileJobOutputs.Length;
|
||||
int areaQueryCount = _areaCollisionRequests.Count;
|
||||
if (projectileCount == 0 && areaQueryCount == 0)
|
||||
{
|
||||
ResetCollisionRuntimeStats();
|
||||
return;
|
||||
}
|
||||
|
||||
float queryRadius = Mathf.Max(0.01f, _projectileCollisionQueryRadius);
|
||||
int maxCandidatesPerQuery = Mathf.Max(1, _projectileMaxCandidatesPerQuery);
|
||||
float maxQueryRadius = queryRadius;
|
||||
int queryId = 0;
|
||||
int projectileQueryCount = 0;
|
||||
int builtAreaQueryCount = 0;
|
||||
using (CustomProfilerMarker.Collision_BuildQueries.Auto())
|
||||
{
|
||||
for (int i = 0; i < projectileCount; i++)
|
||||
{
|
||||
ProjectileJobOutputData projectile = _projectileJobOutputs[i];
|
||||
if (!projectile.Active || projectile.State != ProjectileStateActive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddProjectileCollisionQuery(queryId, in projectile, queryRadius, maxCandidatesPerQuery);
|
||||
queryId++;
|
||||
projectileQueryCount++;
|
||||
}
|
||||
|
||||
for (int i = 0; i < areaQueryCount; i++)
|
||||
{
|
||||
AreaCollisionRequestData request = _areaCollisionRequests[i];
|
||||
AddAreaCollisionQuery(queryId, request.SourceEntityId, request.SourceOwnerEntityId,
|
||||
request.SourceWasActiveAtQueryTime, in request.Center, request.Radius, request.MaxTargets,
|
||||
request.ShapeType, in request.Direction, request.HalfAngleDeg, request.HalfWidth,
|
||||
request.HalfLength);
|
||||
queryId++;
|
||||
builtAreaQueryCount++;
|
||||
if (request.Radius > maxQueryRadius)
|
||||
{
|
||||
maxQueryRadius = request.Radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_lastProjectileCollisionQueryCount = projectileQueryCount;
|
||||
_lastAreaCollisionQueryCount = builtAreaQueryCount;
|
||||
_lastCollisionQueryCount = projectileQueryCount + builtAreaQueryCount;
|
||||
_lastResolvedAreaHitCount = 0;
|
||||
|
||||
if (_collisionQueryInputs.Length == 0)
|
||||
{
|
||||
_lastCollisionCandidateCount = 0;
|
||||
_lastProjectileCollisionCandidateCount = 0;
|
||||
_lastAreaCollisionCandidateCount = 0;
|
||||
_lastCollisionCellSize = 0f;
|
||||
_lastCollisionHasEnemyTargets = _enemyJobOutputs.Length > 0;
|
||||
return;
|
||||
}
|
||||
|
||||
float autoCellSize = maxQueryRadius * 2f;
|
||||
float configuredCellSize = _projectileCollisionCellSize > 0f ? _projectileCollisionCellSize : autoCellSize;
|
||||
float cellSize = Mathf.Max(0.1f, configuredCellSize);
|
||||
bool hasEnemyTargets = _enemyJobOutputs.Length > 0;
|
||||
_lastCollisionCellSize = cellSize;
|
||||
_lastCollisionHasEnemyTargets = hasEnemyTargets;
|
||||
|
||||
using (CustomProfilerMarker.Collision_BuildBuckets.Auto())
|
||||
{
|
||||
if (hasEnemyTargets)
|
||||
{
|
||||
BuildEnemyCollisionBuckets(cellSize);
|
||||
}
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.Collision_QueryCandidates.Auto())
|
||||
{
|
||||
_collisionCandidateQueryScheduled = ScheduleCollisionCandidateQueryJob(cellSize, hasEnemyTargets,
|
||||
out _collisionCandidateQueryHandle);
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteCollisionCandidatesForFrame()
|
||||
{
|
||||
if (_collisionCandidateQueryScheduled)
|
||||
{
|
||||
_collisionCandidateQueryHandle.Complete();
|
||||
_collisionCandidateQueryScheduled = false;
|
||||
}
|
||||
|
||||
CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount);
|
||||
_lastProjectileCollisionCandidateCount = projectileCandidateCount;
|
||||
_lastAreaCollisionCandidateCount = areaCandidateCount;
|
||||
_lastCollisionCandidateCount = projectileCandidateCount + areaCandidateCount;
|
||||
}
|
||||
|
||||
private void BuildEnemyCollisionBuckets(float cellSize)
|
||||
{
|
||||
_enemyCollisionBuckets.Clear();
|
||||
int enemyCount = _enemyJobOutputs.Length;
|
||||
if (enemyCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BuildCollisionBucketsBurstJob job = new BuildCollisionBucketsBurstJob
|
||||
{
|
||||
EnemyOutputs = _enemyJobOutputs.AsArray(),
|
||||
Buckets = _enemyCollisionBuckets.AsParallelWriter(),
|
||||
CellSize = cellSize
|
||||
};
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
|
||||
{
|
||||
JobHandle handle = job.Schedule(enemyCount, 64);
|
||||
handle.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
private struct BuildCollisionBucketsBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobOutputData> EnemyOutputs;
|
||||
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
|
||||
public float CellSize;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
EnemyJobOutputData enemy = EnemyOutputs[index];
|
||||
int cellX = (int)math.floor(enemy.Position.x / CellSize);
|
||||
int cellZ = (int)math.floor(enemy.Position.z / CellSize);
|
||||
Buckets.Add(SeparationCellKey(cellX, cellZ), index);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ScheduleCollisionCandidateQueryJob(float cellSize, bool hasEnemyTargets, out JobHandle handle)
|
||||
{
|
||||
handle = default;
|
||||
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_collisionCandidates.Clear();
|
||||
int queryCount = _collisionQueryInputs.Length;
|
||||
if (queryCount == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasPlayerTarget = TryGetPlayerCollisionTarget(out int playerTargetEntityId, out float3 playerPosition);
|
||||
QueryCollisionCandidatesBurstJob job = new QueryCollisionCandidatesBurstJob
|
||||
{
|
||||
Queries = _collisionQueryInputs.AsArray(),
|
||||
EnemyBuckets = _enemyCollisionBuckets,
|
||||
EnemyOutputs = _enemyJobOutputs.AsArray(),
|
||||
Candidates = _collisionCandidates.AsParallelWriter(),
|
||||
HasEnemyTargets = hasEnemyTargets,
|
||||
HasPlayerTarget = hasPlayerTarget,
|
||||
PlayerTargetEntityId = playerTargetEntityId,
|
||||
PlayerPosition = playerPosition,
|
||||
CellSize = cellSize
|
||||
};
|
||||
|
||||
handle = job.Schedule(queryCount, 64);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount)
|
||||
{
|
||||
projectileCandidateCount = 0;
|
||||
areaCandidateCount = 0;
|
||||
|
||||
if (!_collisionCandidates.IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _collisionCandidates.Length; i++)
|
||||
{
|
||||
CollisionCandidateData candidate = _collisionCandidates[i];
|
||||
if (candidate.SourceType == CollisionSourceTypeProjectile)
|
||||
{
|
||||
projectileCandidateCount++;
|
||||
}
|
||||
else if (candidate.SourceType == CollisionSourceTypeArea)
|
||||
{
|
||||
areaCandidateCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetPlayerCollisionTarget(out int playerEntityId, out float3 playerPosition)
|
||||
{
|
||||
playerEntityId = PlayerEntityId;
|
||||
playerPosition = default;
|
||||
|
||||
if (!TryGetAliveTargetableEntity(playerEntityId, out TargetableObject playerTarget))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform playerTransform = playerTarget.CachedTransform;
|
||||
if (playerTransform == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3 position = playerTransform.position;
|
||||
playerPosition = new float3(position.x, position.y, position.z);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac6aca660ff944e69ea8f5e06f0609cf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
// Shared collision pipeline configuration and runtime state.
|
||||
// Request buffering, broad-phase scheduling, resolve, and presentation
|
||||
// dispatch live in dedicated partial files under Jobs/.
|
||||
private const int PlayerEntityId = -1;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87bc5c5ec75920d46a65881e949bd2a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
using CustomEvent;
|
||||
using SepCore.Definition;
|
||||
using Entity;
|
||||
using Entity.Weapon;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Collision Presentation
|
||||
|
||||
private void DispatchProjectileHitPresentationEvent(int projectileEntityId, int sourceEntityId,
|
||||
int sourceOwnerEntityId, int targetEntityId, int damage, in Vector3 hitPosition)
|
||||
{
|
||||
if (!_dispatchProjectileHitPresentationEvent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
eventComponent.Fire(this, ProjectileHitPresentationEventArgs.Create(
|
||||
projectileEntityId,
|
||||
sourceEntityId,
|
||||
sourceOwnerEntityId,
|
||||
targetEntityId,
|
||||
damage,
|
||||
hitPosition,
|
||||
_dispatchProjectileHitMarkerEvent,
|
||||
_dispatchProjectileHitEffectEvent,
|
||||
_projectileHitPresentationEffectTypeId));
|
||||
}
|
||||
|
||||
private static bool TryResolveImpactSource(EntityBase sourceEntity, EntityBase ownerEntity,
|
||||
out EntityBase attacker, out ImpactData impactData)
|
||||
{
|
||||
if (TryResolveImpactFromEntity(sourceEntity, out impactData))
|
||||
{
|
||||
attacker = sourceEntity;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryResolveImpactFromEntity(ownerEntity, out impactData))
|
||||
{
|
||||
attacker = ownerEntity;
|
||||
return true;
|
||||
}
|
||||
|
||||
attacker = null;
|
||||
impactData = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveImpactFromEntity(EntityBase entity, out ImpactData impactData)
|
||||
{
|
||||
if (entity is WeaponBase weapon)
|
||||
{
|
||||
impactData = weapon.GetImpactData();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entity is EnemyProjectile enemyProjectile)
|
||||
{
|
||||
impactData = enemyProjectile.GetImpactData();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entity is TargetableObject targetableObject)
|
||||
{
|
||||
impactData = targetableObject.GetImpactData();
|
||||
return true;
|
||||
}
|
||||
|
||||
impactData = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetAliveTargetableEntity(int entityId, out TargetableObject target)
|
||||
{
|
||||
target = null;
|
||||
|
||||
var enemyManager = GameEntry.EnemyManager;
|
||||
if (enemyManager != null && enemyManager.TryGetEnemy(entityId, out EntityBase enemyEntity))
|
||||
{
|
||||
if (enemyEntity is TargetableObject enemyTarget && enemyTarget.Available && !enemyTarget.IsDead)
|
||||
{
|
||||
target = enemyTarget;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
EntityBase entity = TryGetEntityById(entityId);
|
||||
if (entity is TargetableObject targetable && targetable.Available && !targetable.IsDead)
|
||||
{
|
||||
target = targetable;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static EntityBase TryGetEntityById(int entityId)
|
||||
{
|
||||
var entityComponent = GameEntry.Entity;
|
||||
return entityComponent != null ? entityComponent.GetGameEntity(entityId) : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 882102e53dea40a2b5b596e1b41bbed6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
using Entity;
|
||||
using Entity.Weapon;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Collision Requests
|
||||
|
||||
public bool TryRequestAreaCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
float radius, int maxTargets = 16)
|
||||
{
|
||||
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
|
||||
maxTargets, CollisionShapeCircle, Vector3.forward, 180f, 0f, 0f);
|
||||
}
|
||||
|
||||
public bool TryRequestSectorCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
float radius, in Vector3 direction, float halfAngleDeg, int maxTargets = 16)
|
||||
{
|
||||
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
|
||||
maxTargets, CollisionShapeSector, direction, halfAngleDeg, 0f, 0f);
|
||||
}
|
||||
|
||||
public bool TryRequestRectangleCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
float halfWidth, float halfLength, in Vector3 direction, int maxTargets = 16)
|
||||
{
|
||||
float safeHalfWidth = Mathf.Max(0.01f, halfWidth);
|
||||
float safeHalfLength = Mathf.Max(0.01f, halfLength);
|
||||
float boundingRadius = Mathf.Sqrt(safeHalfWidth * safeHalfWidth + safeHalfLength * safeHalfLength);
|
||||
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, boundingRadius,
|
||||
maxTargets, CollisionShapeRectangle, direction, 0f, safeHalfWidth, safeHalfLength);
|
||||
}
|
||||
|
||||
private bool TryRequestAreaCollisionInternal(int sourceEntityId, int sourceOwnerEntityId,
|
||||
in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg,
|
||||
float halfWidth, float halfLength)
|
||||
{
|
||||
if (sourceEntityId == 0 || radius <= 0f || maxTargets <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int resolvedOwnerEntityId = sourceOwnerEntityId != 0 ? sourceOwnerEntityId : sourceEntityId;
|
||||
bool sourceWasActiveAtQueryTime = WasCollisionSourceActiveAtQueryTime(sourceEntityId);
|
||||
Vector3 normalizedDirection = direction;
|
||||
normalizedDirection.y = 0f;
|
||||
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
normalizedDirection = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
normalizedDirection.Normalize();
|
||||
}
|
||||
|
||||
_areaCollisionRequests.Add(new AreaCollisionRequestData
|
||||
{
|
||||
SourceEntityId = sourceEntityId,
|
||||
SourceOwnerEntityId = resolvedOwnerEntityId,
|
||||
SourceWasActiveAtQueryTime = sourceWasActiveAtQueryTime,
|
||||
Center = center,
|
||||
Radius = Mathf.Max(0.01f, radius),
|
||||
MaxTargets = Mathf.Max(1, maxTargets),
|
||||
ShapeType = shapeType,
|
||||
Direction = normalizedDirection,
|
||||
HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f),
|
||||
HalfWidth = Mathf.Max(0f, halfWidth),
|
||||
HalfLength = Mathf.Max(0f, halfLength)
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int GetPendingAreaCollisionRequestCount()
|
||||
{
|
||||
return _areaCollisionRequests.Count;
|
||||
}
|
||||
|
||||
private int EstimatePendingAreaCollisionCandidateCountFromRequests()
|
||||
{
|
||||
int expectedCount = 0;
|
||||
for (int i = 0; i < _areaCollisionRequests.Count; i++)
|
||||
{
|
||||
expectedCount += Mathf.Max(1, _areaCollisionRequests[i].MaxTargets);
|
||||
}
|
||||
|
||||
return expectedCount;
|
||||
}
|
||||
|
||||
private void ClearAreaCollisionTransientBuffers()
|
||||
{
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
}
|
||||
|
||||
private static bool WasCollisionSourceActiveAtQueryTime(int sourceEntityId)
|
||||
{
|
||||
EntityBase sourceEntity = TryGetEntityById(sourceEntityId);
|
||||
if (sourceEntity == null || !sourceEntity.Available)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceEntity is WeaponBase weapon)
|
||||
{
|
||||
return weapon.IsAttacking;
|
||||
}
|
||||
|
||||
if (sourceEntity is EnemyProjectile projectile)
|
||||
{
|
||||
return projectile.IsActive;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56086ce3981e4ed5b95fdc54f7db85a3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
using Components;
|
||||
using CustomDebugger;
|
||||
using CustomUtility;
|
||||
using SepCore.Definition;
|
||||
using Entity;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Collision Resolve
|
||||
|
||||
private void ResolveCollisionCandidatesOnMainThread()
|
||||
{
|
||||
if (!_collisionCandidates.IsCreated)
|
||||
{
|
||||
_lastResolvedAreaHitCount = 0;
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
_projectileResolvedEntityIds.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
|
||||
if (_collisionCandidates.Length == 0)
|
||||
{
|
||||
_lastResolvedAreaHitCount = 0;
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.Collision_ResolveProjectile.Auto())
|
||||
{
|
||||
for (int i = 0; i < _collisionCandidates.Length; i++)
|
||||
{
|
||||
CollisionCandidateData candidate = _collisionCandidates[i];
|
||||
if (candidate.SourceType == CollisionSourceTypeProjectile)
|
||||
{
|
||||
int projectileEntityId = candidate.SourceEntityId;
|
||||
if (_projectileResolvedEntityIds.Contains(projectileEntityId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetActiveProjectileSimData(projectileEntityId, out _, out ProjectileSimData projectile))
|
||||
{
|
||||
_projectileResolvedEntityIds.Add(projectileEntityId);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool shouldExpireProjectile = true;
|
||||
bool shouldDispatchPresentation = false;
|
||||
int damage = 0;
|
||||
Vector3 hitPosition = projectile.Position;
|
||||
if (TryGetAliveTargetableEntity(candidate.TargetEntityId, out TargetableObject target))
|
||||
{
|
||||
EntityBase sourceEntity = TryGetEntityById(candidate.SourceEntityId);
|
||||
EntityBase ownerEntity = TryGetEntityById(candidate.SourceOwnerEntityId);
|
||||
shouldExpireProjectile = ResolveProjectileHitAgainstTarget(target, sourceEntity,
|
||||
ownerEntity,
|
||||
in projectile,
|
||||
out damage, out hitPosition, out shouldDispatchPresentation);
|
||||
}
|
||||
|
||||
if (shouldDispatchPresentation)
|
||||
{
|
||||
DispatchProjectileHitPresentationEvent(projectileEntityId, candidate.SourceEntityId,
|
||||
candidate.SourceOwnerEntityId, candidate.TargetEntityId, damage, in hitPosition);
|
||||
}
|
||||
|
||||
if (shouldExpireProjectile)
|
||||
{
|
||||
MarkProjectileAsExpired(projectileEntityId);
|
||||
_projectileResolvedEntityIds.Add(projectileEntityId);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate.SourceType == CollisionSourceTypeArea)
|
||||
{
|
||||
long dedupKey = (((long)candidate.QueryId) << 32) ^ (uint)candidate.TargetEntityId;
|
||||
if (!_areaCollisionHitDedupKeys.Add(dedupKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_areaCollisionHitEvents.Add(new AreaCollisionHitEventData
|
||||
{
|
||||
QueryId = candidate.QueryId,
|
||||
SourceEntityId = candidate.SourceEntityId,
|
||||
SourceOwnerEntityId = candidate.SourceOwnerEntityId,
|
||||
TargetEntityId = candidate.TargetEntityId,
|
||||
SqrDistance = candidate.SqrDistance
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int resolvedAreaHitCount;
|
||||
using (CustomProfilerMarker.Collision_ResolveArea.Auto())
|
||||
{
|
||||
resolvedAreaHitCount = ResolveAreaCollisionHitsOnMainThread();
|
||||
}
|
||||
|
||||
_lastResolvedAreaHitCount = resolvedAreaHitCount;
|
||||
_projectileResolvedEntityIds.Clear();
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
}
|
||||
|
||||
private bool ResolveProjectileHitAgainstTarget(TargetableObject target, EntityBase sourceEntity,
|
||||
EntityBase ownerEntity,
|
||||
in ProjectileSimData projectile, out int damage, out Vector3 hitPosition,
|
||||
out bool shouldDispatchPresentation)
|
||||
{
|
||||
damage = 0;
|
||||
hitPosition = projectile.Position;
|
||||
shouldDispatchPresentation = false;
|
||||
|
||||
if (target == null || !target.Available || target.IsDead)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target.CachedTransform != null)
|
||||
{
|
||||
hitPosition = target.CachedTransform.position;
|
||||
}
|
||||
|
||||
if (!TryResolveImpactSource(sourceEntity, ownerEntity, out EntityBase attacker,
|
||||
out ImpactData sourceImpact))
|
||||
{
|
||||
shouldDispatchPresentation = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
ImpactData targetImpact = target.GetImpactData();
|
||||
if (AIUtility.GetRelation(targetImpact.Camp, sourceImpact.Camp) == RelationType.Friendly)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
damage = AIUtility.CalcDamageHP(sourceImpact.AttackBase, sourceImpact.AttackStat,
|
||||
targetImpact.DefenseStat,
|
||||
targetImpact.DodgeStat);
|
||||
shouldDispatchPresentation = true;
|
||||
if (damage <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
target.ApplyDamage(attacker ?? sourceEntity ?? ownerEntity, damage);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int ResolveAreaCollisionHitsOnMainThread()
|
||||
{
|
||||
if (_areaCollisionHitEvents.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int resolvedHitCount = 0;
|
||||
for (int i = 0; i < _areaCollisionHitEvents.Count; i++)
|
||||
{
|
||||
AreaCollisionHitEventData hitEvent = _areaCollisionHitEvents[i];
|
||||
if (!TryGetCollisionQueryByQueryId(hitEvent.QueryId, out CollisionQueryData query) ||
|
||||
query.SourceType != CollisionSourceTypeArea)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!query.SourceWasActiveAtQueryTime)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EntityBase sourceEntity = TryGetEntityById(hitEvent.SourceEntityId);
|
||||
if (sourceEntity == null || !sourceEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetAliveTargetableEntity(hitEvent.TargetEntityId, out TargetableObject target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float targetRadius = ResolveAreaTargetRadius(target);
|
||||
if (!IsAreaTargetInsidePreciseShape(in query, target, targetRadius))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AIUtility.PerformCollision(target, sourceEntity, true);
|
||||
resolvedHitCount++;
|
||||
}
|
||||
|
||||
return resolvedHitCount;
|
||||
}
|
||||
|
||||
private bool TryGetCollisionQueryByQueryId(int queryId, out CollisionQueryData query)
|
||||
{
|
||||
query = default;
|
||||
if (!_collisionQueryInputs.IsCreated || queryId < 0 || queryId >= _collisionQueryInputs.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CollisionQueryData direct = _collisionQueryInputs[queryId];
|
||||
if (direct.QueryId == queryId)
|
||||
{
|
||||
query = direct;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _collisionQueryInputs.Length; i++)
|
||||
{
|
||||
CollisionQueryData candidate = _collisionQueryInputs[i];
|
||||
if (candidate.QueryId != queryId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
query = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private float ResolveAreaTargetRadius(TargetableObject target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (target is EnemyBase && TryGetEnemyData(target.Id, out EnemySimData enemyData))
|
||||
{
|
||||
return Mathf.Max(0f, enemyData.EnemyBodyRadius);
|
||||
}
|
||||
|
||||
MovementComponent movementComponent = target.GetComponent<MovementComponent>();
|
||||
return movementComponent != null ? Mathf.Max(0f, movementComponent.EnemyBodyRadius) : 0f;
|
||||
}
|
||||
|
||||
private static bool IsAreaTargetInsidePreciseShape(in CollisionQueryData query, TargetableObject target,
|
||||
float targetRadius)
|
||||
{
|
||||
if (target == null || target.CachedTransform == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3 center = new Vector3(query.Position.x, query.Position.y, query.Position.z);
|
||||
Vector3 toTarget = target.CachedTransform.position - center;
|
||||
toTarget.y = 0f;
|
||||
|
||||
float radius = Mathf.Max(0.01f, query.Radius + Mathf.Max(0f, targetRadius));
|
||||
float radiusSqr = radius * radius;
|
||||
float sqrDistance = toTarget.sqrMagnitude;
|
||||
if (sqrDistance > radiusSqr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query.ShapeType == CollisionShapeRectangle)
|
||||
{
|
||||
Vector3 forwardRect = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z);
|
||||
forwardRect.y = 0f;
|
||||
if (forwardRect.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
forwardRect = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
forwardRect.Normalize();
|
||||
}
|
||||
|
||||
Vector3 rightRect = Vector3.Cross(Vector3.up, forwardRect);
|
||||
float halfWidth = Mathf.Max(0.01f, query.HalfWidth + Mathf.Max(0f, targetRadius));
|
||||
float halfLength = Mathf.Max(0.01f, query.HalfLength + Mathf.Max(0f, targetRadius));
|
||||
float forwardDistance = Vector3.Dot(toTarget, forwardRect);
|
||||
float lateralDistance = Vector3.Dot(toTarget, rightRect);
|
||||
return Mathf.Abs(forwardDistance) <= halfLength && Mathf.Abs(lateralDistance) <= halfWidth;
|
||||
}
|
||||
|
||||
if (query.ShapeType != CollisionShapeSector)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sqrDistance <= Mathf.Epsilon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector3 forward = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z);
|
||||
forward.y = 0f;
|
||||
if (forward.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
forward = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
forward.Normalize();
|
||||
}
|
||||
|
||||
float halfAngle = Mathf.Clamp(query.HalfAngleDeg, 0f, 180f);
|
||||
float angle = Vector3.Angle(forward, toTarget.normalized);
|
||||
return angle <= halfAngle;
|
||||
}
|
||||
|
||||
private bool TryGetActiveProjectileSimData(int projectileEntityId, out int simulationIndex,
|
||||
out ProjectileSimData projectile)
|
||||
{
|
||||
simulationIndex = -1;
|
||||
projectile = default;
|
||||
|
||||
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int foundIndex) || foundIndex < 0 ||
|
||||
foundIndex >= _projectiles.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ProjectileSimData data = _projectiles[foundIndex];
|
||||
if (!data.Active || data.State != ProjectileStateActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
simulationIndex = foundIndex;
|
||||
projectile = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MarkProjectileAsExpired(int projectileEntityId)
|
||||
{
|
||||
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int simulationIndex) ||
|
||||
simulationIndex < 0 || simulationIndex >= _projectiles.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ProjectileSimData projectile = _projectiles[simulationIndex];
|
||||
projectile.Active = false;
|
||||
projectile.State = ProjectileStateExpired;
|
||||
projectile.RemainingLifetime = 0f;
|
||||
if (projectile.LifeTime > 0f && projectile.Age < projectile.LifeTime)
|
||||
{
|
||||
projectile.Age = projectile.LifeTime;
|
||||
}
|
||||
|
||||
_projectiles[simulationIndex] = projectile;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5261fa73dd9c4bfc91fcba0d5ca1bcb1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
using CustomDebugger;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
// Orchestrates per-tick simulation pipeline and enemy movement/separation jobs.
|
||||
[Header("敌人互斥参数")]
|
||||
[Tooltip("敌人互斥分桶使用的网格尺寸。小于等于 0 时,将根据敌人体积半径自动计算。")]
|
||||
[SerializeField]
|
||||
private float _enemySeparationCellSize = 0f;
|
||||
|
||||
[Tooltip("每次迭代对互斥推力累积值的阻尼系数。数值越大,分离速度越快。")]
|
||||
[SerializeField]
|
||||
private float _enemySeparationPushDamping = 0.75f;
|
||||
|
||||
[Tooltip("每次迭代允许的最大互斥位移步长(按敌人体积半径倍率计算)。")]
|
||||
[SerializeField]
|
||||
private float _enemySeparationMaxStepScale = 1f;
|
||||
|
||||
[Tooltip("敌人进入攻击范围后,互斥位移是否保持为相对玩家方向的切向分量(避免被径向推离玩家)。")]
|
||||
[SerializeField]
|
||||
private bool _enemySeparationUseTangentialInAttackRange = true;
|
||||
|
||||
[Tooltip("互斥推力方向突变时的时间平滑系数。越大越稳定,但响应越慢。")]
|
||||
[SerializeField]
|
||||
private float _enemySeparationPushSmoothing = 0.55f;
|
||||
|
||||
private void TickSimulationPipeline(in SimulationTickContext context)
|
||||
{
|
||||
// 1. 早退分支:deltaTime <= 0 时只清理碰撞通道和统计,然后返回。
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
PrepareCollisionQueryAndCandidateChannels(0, 0, 0);
|
||||
ResetCollisionRuntimeStats();
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
JobHandle enemyMovementHandle = default;
|
||||
JobHandle projectileMovementHandle = default;
|
||||
JobHandle enemySeparationHandle = default;
|
||||
bool hasEnemySeparationJob = false;
|
||||
bool hasEnemySeparationCandidates = false;
|
||||
int enemySeparationCount = 0;
|
||||
float enemySeparationMaxRadius = 0.45f;
|
||||
|
||||
// 2. BuildInput 阶段:
|
||||
// 把 _enemies/_projectiles 同步到 Native 输入,准备输出缓冲;
|
||||
// 统计是否需要敌人分离 Job;
|
||||
// 预估并准备碰撞查询/候选缓冲。
|
||||
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
||||
{
|
||||
SyncSimulationStateToJobInputs();
|
||||
int enemyCount = _enemyJobInputs.Length;
|
||||
int projectileCount = _projectileJobInputs.Length;
|
||||
PrepareEnemyJobOutputBuffer(enemyCount);
|
||||
PrepareProjectileJobOutputBuffer(projectileCount);
|
||||
|
||||
enemySeparationCount = enemyCount;
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
EnemyJobInputData input = _enemyJobInputs[i];
|
||||
if (!input.AvoidEnemyOverlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasEnemySeparationCandidates = true;
|
||||
float radius = input.EnemyBodyRadius > 0f ? input.EnemyBodyRadius : 0.45f;
|
||||
if (radius > enemySeparationMaxRadius)
|
||||
{
|
||||
enemySeparationMaxRadius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEnemySeparationCandidates)
|
||||
{
|
||||
int separationBucketCapacity = Mathf.Max(128, enemyCount * 2);
|
||||
PrepareEnemySeparationJobBuffers(enemyCount, separationBucketCapacity);
|
||||
}
|
||||
|
||||
int projectileQueryCount = _projectiles.Count;
|
||||
int areaQueryCount = GetPendingAreaCollisionRequestCount();
|
||||
int queryCount = projectileQueryCount + areaQueryCount;
|
||||
int projectileExpectedCount = projectileQueryCount * Mathf.Max(1, _projectileMaxCandidatesPerQuery);
|
||||
int areaExpectedCount = EstimatePendingAreaCollisionCandidateCountFromRequests();
|
||||
int expectedCandidateCount = Mathf.Max(16, projectileExpectedCount + areaExpectedCount);
|
||||
int bucketCapacity = Mathf.Max(256, _enemies.Count * 2 + queryCount);
|
||||
PrepareCollisionQueryAndCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity);
|
||||
}
|
||||
|
||||
// 3. StateUpdate 阶段(调度两个移动 Job)
|
||||
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
||||
{
|
||||
enemyMovementHandle = ExecuteEnemyMovementJob(in context);
|
||||
projectileMovementHandle = ExecuteProjectileMovementJob(in context);
|
||||
}
|
||||
|
||||
// 4. Schedule 阶段(可选分离 Job)
|
||||
// 如果有需要分离的敌人,调度:
|
||||
// BuildEnemySeparationBucketsBurstJob(依赖 EnemyMovement)
|
||||
// EnemySeparationBurstJob(依赖分桶 Job)
|
||||
// 然后把“敌人链路 handle”和“投射物移动 handle”合并。
|
||||
JobHandle simulationHandle;
|
||||
using (CustomProfilerMarker.TickEnemies_Schedule.Auto())
|
||||
{
|
||||
hasEnemySeparationJob = TryScheduleEnemySeparationFromJobOutput(
|
||||
in context,
|
||||
enemyMovementHandle,
|
||||
hasEnemySeparationCandidates,
|
||||
enemySeparationCount,
|
||||
enemySeparationMaxRadius,
|
||||
out enemySeparationHandle);
|
||||
JobHandle enemyHandle = hasEnemySeparationJob ? enemySeparationHandle : enemyMovementHandle;
|
||||
simulationHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle);
|
||||
}
|
||||
|
||||
// 5. Complete 阶段:等待上述 Job 全部完成。
|
||||
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
|
||||
{
|
||||
simulationHandle.Complete();
|
||||
}
|
||||
|
||||
// 6. 主线程后处理阶段:
|
||||
// - 把分离结果覆盖回敌人输出(如果有分离 Job)
|
||||
// - 构建碰撞候选(投射物查询 + area 请求查询 + 网格筛选)
|
||||
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
|
||||
{
|
||||
if (hasEnemySeparationJob)
|
||||
{
|
||||
CommitEnemySeparationFromJobOutput(enemySeparationCount);
|
||||
}
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.Collision.Auto())
|
||||
{
|
||||
PrepareCollisionCandidatesForFrame();
|
||||
CompleteCollisionCandidatesForFrame();
|
||||
}
|
||||
|
||||
// 7. MainThreadCommit 阶段
|
||||
// - ApplyJobOutputsToSimulationState(写回 _enemies/_projectiles)
|
||||
// - ResolveCollisionCandidatesOnMainThread(命中结算、事件、范围碰撞)
|
||||
// - RecycleInactiveAndExpiredProjectiles(隐藏并移除失效投射物)
|
||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||
{
|
||||
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
|
||||
{
|
||||
ApplyJobOutputsToSimulationState();
|
||||
ResolveCollisionCandidatesOnMainThread();
|
||||
RecycleInactiveAndExpiredProjectiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context)
|
||||
{
|
||||
int enemyCount = _enemyJobInputs.Length;
|
||||
if (enemyCount == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
CopyEnemyInputsToOutputs();
|
||||
return default;
|
||||
}
|
||||
|
||||
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
|
||||
NativeArray<EnemyJobInputData> inputArray = _enemyJobInputs.AsArray();
|
||||
NativeArray<EnemyJobOutputData> outputArray = _enemyJobOutputs.AsArray();
|
||||
|
||||
EnemyMovementBurstJob burstJob = new EnemyMovementBurstJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Outputs = outputArray,
|
||||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition
|
||||
};
|
||||
return burstJob.Schedule(enemyCount, 64);
|
||||
}
|
||||
|
||||
private void CopyEnemyInputsToOutputs()
|
||||
{
|
||||
for (int i = 0; i < _enemyJobInputs.Length; i++)
|
||||
{
|
||||
EnemyJobInputData input = _enemyJobInputs[i];
|
||||
_enemyJobOutputs[i] = new EnemyJobOutputData
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
Position = input.Position,
|
||||
Forward = input.Forward,
|
||||
Rotation = input.Rotation,
|
||||
Speed = input.Speed,
|
||||
AttackRange = input.AttackRange,
|
||||
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = input.EnemyBodyRadius,
|
||||
SeparationIterations = input.SeparationIterations,
|
||||
TargetType = input.TargetType,
|
||||
State = input.State
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryScheduleEnemySeparationFromJobOutput(in SimulationTickContext context, JobHandle dependency,
|
||||
bool hasSeparationCandidates, int enemyCount, float maxRadius, out JobHandle separationHandle)
|
||||
{
|
||||
separationHandle = dependency;
|
||||
if (enemyCount <= 0 || !hasSeparationCandidates)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float autoCellSize = maxRadius * 2f;
|
||||
float configuredCellSize = _enemySeparationCellSize > 0f ? _enemySeparationCellSize : autoCellSize;
|
||||
float cellSize = Mathf.Max(0.1f, configuredCellSize);
|
||||
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
|
||||
float pushDamping = Mathf.Clamp(_enemySeparationPushDamping, 0f, 2f);
|
||||
float maxStepScale = Mathf.Max(0.1f, _enemySeparationMaxStepScale);
|
||||
bool useTangentialInAttackRange = _enemySeparationUseTangentialInAttackRange;
|
||||
float pushSmoothing = Mathf.Clamp01(_enemySeparationPushSmoothing);
|
||||
|
||||
NativeArray<EnemyJobOutputData> inputArray = _enemyJobOutputs.AsArray();
|
||||
NativeArray<EnemyJobOutputData> separatedOutputArray = _enemyJobSeparationOutputs.AsArray();
|
||||
NativeArray<float2> previousPushes = _enemySeparationPreviousPushes.AsArray();
|
||||
NativeArray<float2> currentPushes = _enemySeparationCurrentPushes.AsArray();
|
||||
|
||||
|
||||
BuildEnemySeparationBucketsBurstJob buildJob = new BuildEnemySeparationBucketsBurstJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Buckets = _enemySeparationBuckets.AsParallelWriter(),
|
||||
CellSize = cellSize
|
||||
};
|
||||
JobHandle buildHandle = buildJob.Schedule(enemyCount, 64, dependency);
|
||||
EnemySeparationBurstJob separationJob = new EnemySeparationBurstJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Buckets = _enemySeparationBuckets,
|
||||
PreviousPushes = previousPushes,
|
||||
Outputs = separatedOutputArray,
|
||||
CurrentPushes = currentPushes,
|
||||
CellSize = cellSize,
|
||||
MaxRadius = maxRadius,
|
||||
PlayerPosition = playerPosition,
|
||||
PushDamping = pushDamping,
|
||||
MaxStepScale = maxStepScale,
|
||||
UseTangentialInAttackRange = useTangentialInAttackRange,
|
||||
PushSmoothing = pushSmoothing
|
||||
};
|
||||
separationHandle = separationJob.Schedule(enemyCount, 64, buildHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CommitEnemySeparationFromJobOutput(int enemyCount)
|
||||
{
|
||||
if (enemyCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CommitEnemySeparationTemporalBuffers(enemyCount);
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
_enemyJobOutputs[i] = _enemyJobSeparationOutputs[i];
|
||||
}
|
||||
}
|
||||
|
||||
private static long SeparationCellKey(int x, int z)
|
||||
{
|
||||
return ((long)x << 32) ^ (uint)z;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99b885bb5cfe48e7bd9dba74fe683149
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[Header("Projectile Simulation")]
|
||||
[Tooltip("Recycle projectile when horizontal distance to player exceeds this range. <=0 disables this rule.")]
|
||||
[SerializeField]
|
||||
private float _projectileMaxDistanceFromPlayer = 120f;
|
||||
|
||||
[Tooltip("Recycle projectile when vertical offset to player exceeds this range. <=0 disables this rule.")]
|
||||
[SerializeField]
|
||||
private float _projectileMaxVerticalOffsetFromPlayer = 30f;
|
||||
|
||||
#region Projectile Movement Job
|
||||
|
||||
private JobHandle ExecuteProjectileMovementJob(in SimulationTickContext context)
|
||||
{
|
||||
int projectileCount = _projectileJobInputs.Length;
|
||||
if (projectileCount == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
CopyProjectileInputsToOutputs();
|
||||
return default;
|
||||
}
|
||||
|
||||
float maxDistance = Mathf.Max(0f, _projectileMaxDistanceFromPlayer);
|
||||
float maxSqrDistanceFromPlayer = maxDistance > 0f ? maxDistance * maxDistance : -1f;
|
||||
float maxVerticalOffsetFromPlayer = Mathf.Max(0f, _projectileMaxVerticalOffsetFromPlayer);
|
||||
float3 playerPosition =
|
||||
new float3(context.PlayerPosition.x, context.PlayerPosition.y, context.PlayerPosition.z);
|
||||
NativeArray<ProjectileJobInputData> inputArray = _projectileJobInputs.AsArray();
|
||||
NativeArray<ProjectileJobOutputData> outputArray = _projectileJobOutputs.AsArray();
|
||||
|
||||
|
||||
ProjectileMovementBurstJob burstJob = new ProjectileMovementBurstJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Outputs = outputArray,
|
||||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition,
|
||||
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
|
||||
MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer
|
||||
};
|
||||
return burstJob.Schedule(projectileCount, 64);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Projectile Cleanup
|
||||
|
||||
private void RecycleInactiveAndExpiredProjectiles()
|
||||
{
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
{
|
||||
ProjectileSimData projectile = _projectiles[i];
|
||||
if (!ShouldRecycleProjectileSimData(projectile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_projectileRecycleEntityIds.Add(projectile.EntityId);
|
||||
}
|
||||
|
||||
if (_projectileRecycleEntityIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entityComponent = GameEntry.Entity;
|
||||
for (int i = 0; i < _projectileRecycleEntityIds.Count; i++)
|
||||
{
|
||||
int entityId = _projectileRecycleEntityIds[i];
|
||||
if (entityComponent != null)
|
||||
{
|
||||
entityComponent.HideEntity(entityId);
|
||||
}
|
||||
|
||||
RemoveProjectileByEntityId(entityId);
|
||||
}
|
||||
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
}
|
||||
|
||||
private static bool ShouldRecycleProjectileSimData(in ProjectileSimData projectile)
|
||||
{
|
||||
if (!projectile.Active)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (projectile.State == ProjectileStateExpired)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return projectile.LifeTime > 0f && projectile.Age >= projectile.LifeTime;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcb6513f18404795a654500d3d2f8ef9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bf448696a8641268abb660b16def6f5
|
||||
timeCreated: 1771911417
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
using CustomEvent;
|
||||
using Entity;
|
||||
using Entity.EntityData;
|
||||
using Entity.Weapon;
|
||||
using GameFramework.Event;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[Header("投射物命中表现")]
|
||||
[Tooltip("是否监听投射物命中表现事件。")]
|
||||
[SerializeField]
|
||||
private bool _projectileHitPresentationEnabled = true;
|
||||
|
||||
[Tooltip("是否播放投射物命中标记。")]
|
||||
[SerializeField]
|
||||
private bool _projectileHitMarkerEnabled = true;
|
||||
|
||||
[Tooltip("命中标记尺寸。")][SerializeField] private float _projectileHitMarkerSize = 0.2f;
|
||||
|
||||
[Tooltip("命中标记在目标上的高度偏移。")]
|
||||
[SerializeField]
|
||||
private float _projectileHitMarkerYOffset = 1.2f;
|
||||
|
||||
[Tooltip("命中标记持续时间。")]
|
||||
[SerializeField]
|
||||
private float _projectileHitMarkerDuration = 0.15f;
|
||||
|
||||
[Tooltip("命中标记颜色。")][SerializeField] private Color _projectileHitMarkerColor = new(1f, 0f, 0f, 0.95f);
|
||||
|
||||
[Tooltip("是否播放投射物命中特效实体。")]
|
||||
[SerializeField]
|
||||
private bool _projectileHitEffectEnabled;
|
||||
|
||||
[Tooltip("投射物命中特效实体类型 Id(<=0 表示不启用)。")]
|
||||
[SerializeField]
|
||||
private int _projectileHitEffectTypeId;
|
||||
|
||||
private sealed class HitPresentation
|
||||
{
|
||||
private readonly SimulationWorld _world;
|
||||
private HandgunHitMarkerAttackEffect _projectileHitMarkerEffect;
|
||||
private bool _isProjectileHitEventSubscribed;
|
||||
|
||||
public HitPresentation(SimulationWorld world)
|
||||
{
|
||||
_world = world;
|
||||
}
|
||||
|
||||
public void OnStart()
|
||||
{
|
||||
if (_world == null || !_world._projectileHitPresentationEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
eventComponent.Subscribe(ProjectileHitPresentationEventArgs.EventId, OnProjectileHitPresentationEvent);
|
||||
_isProjectileHitEventSubscribed = true;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_isProjectileHitEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent != null)
|
||||
{
|
||||
eventComponent.Unsubscribe(ProjectileHitPresentationEventArgs.EventId,
|
||||
OnProjectileHitPresentationEvent);
|
||||
}
|
||||
|
||||
_isProjectileHitEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void OnProjectileHitPresentationEvent(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!_isProjectileHitEventSubscribed || _world == null ||
|
||||
e is not ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ShowHitMarker && _world._projectileHitMarkerEnabled)
|
||||
{
|
||||
PlayHitMarker(args);
|
||||
}
|
||||
|
||||
if (args.ShowHitEffect && _world._projectileHitEffectEnabled)
|
||||
{
|
||||
PlayHitEffect(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayHitMarker(ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
// Projectile hit markers were reusing the handgun marker effect and were
|
||||
// visually indistinguishable from handgun lock/hit feedback.
|
||||
}
|
||||
|
||||
private void PlayHitEffect(ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
int effectTypeId = args.EffectEntityTypeId > 0
|
||||
? args.EffectEntityTypeId
|
||||
: _world._projectileHitEffectTypeId;
|
||||
if (effectTypeId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entityComponent = GameEntry.Entity;
|
||||
if (entityComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EffectData effectData = new EffectData(entityComponent.GenerateSerialId(), effectTypeId)
|
||||
{
|
||||
Position = args.HitPosition
|
||||
};
|
||||
entityComponent.ShowEffect(effectData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99cec505913140a2b4bdf1498b28c044
|
||||
timeCreated: 1771911517
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed4e018208ef0d042b99372c1f9ae778
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
using Entity;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private sealed class TransformSync
|
||||
{
|
||||
private readonly SimulationWorld _world;
|
||||
|
||||
public TransformSync(SimulationWorld world)
|
||||
{
|
||||
_world = world;
|
||||
}
|
||||
|
||||
public void OnLateUpdate()
|
||||
{
|
||||
if (_world == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enemyManager = GameEntry.EnemyManager;
|
||||
if (enemyManager != null && enemyManager.Enemies != null)
|
||||
{
|
||||
var enemies = enemyManager.Enemies;
|
||||
foreach (var enemy in enemies)
|
||||
{
|
||||
if (enemy is not EnemyBase enemyEntity || !enemyEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_world.TryGetEnemyData(enemyEntity.Id, out EnemySimData enemyData))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyEnemyPresentation(enemyEntity, enemyData);
|
||||
}
|
||||
}
|
||||
|
||||
var projectiles = _world._projectiles;
|
||||
for (int i = 0; i < projectiles.Count; i++)
|
||||
{
|
||||
ProjectileSimData projectileData = projectiles[i];
|
||||
if (!projectileData.Active || projectileData.State != ProjectileStateActive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EntityBase projectileEntity = TryGetEntityById(projectileData.EntityId);
|
||||
if (projectileEntity == null || !projectileEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyProjectilePresentation(projectileEntity, projectileData);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData)
|
||||
{
|
||||
Transform enemyTransform = enemyEntity.CachedTransform;
|
||||
enemyTransform.position = enemyData.Position;
|
||||
|
||||
Quaternion rotation = enemyData.Rotation;
|
||||
float rotationMagnitude = Mathf.Abs(rotation.x) + Mathf.Abs(rotation.y) + Mathf.Abs(rotation.z) +
|
||||
Mathf.Abs(rotation.w);
|
||||
if (rotationMagnitude > float.Epsilon)
|
||||
{
|
||||
enemyTransform.rotation = rotation;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 forward = enemyData.Forward;
|
||||
forward.y = 0f;
|
||||
if (forward.sqrMagnitude > float.Epsilon)
|
||||
{
|
||||
enemyTransform.forward = forward.normalized;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyProjectilePresentation(EntityBase projectileEntity,
|
||||
in ProjectileSimData projectileData)
|
||||
{
|
||||
Transform projectileTransform = projectileEntity.CachedTransform;
|
||||
if (projectileTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
projectileTransform.position = projectileData.Position;
|
||||
Vector3 forward = projectileData.Forward;
|
||||
if (forward.sqrMagnitude <= float.Epsilon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
projectileTransform.rotation = Quaternion.LookRotation(forward.normalized, Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21da1bcb02d247358738e9b562af5512
|
||||
timeCreated: 1771911431
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2d0f6135bae4675af78194b2272e280
|
||||
timeCreated: 1771590468
|
||||
@@ -0,0 +1,19 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public struct EnemySimData
|
||||
{
|
||||
public int EntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public Quaternion Rotation;
|
||||
public float Speed;
|
||||
public float AttackRange;
|
||||
public bool AvoidEnemyOverlap;
|
||||
public float EnemyBodyRadius;
|
||||
public int SeparationIterations;
|
||||
public int TargetType;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef2eea85685544189eef2d9f2cece080
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public struct PickupSimData
|
||||
{
|
||||
public int EntityId;
|
||||
public Vector3 Position;
|
||||
public float PickupRadius;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff6e8b75e4af4c3ca9e3b732b1383f95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public struct ProjectileSimData
|
||||
{
|
||||
public int EntityId;
|
||||
public int OwnerEntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public Vector3 Velocity;
|
||||
public float Speed;
|
||||
public float LifeTime;
|
||||
public float Age;
|
||||
public bool Active;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2eec1f4389004d69bdce8c4dd95d255e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public readonly struct SimulationTickContext
|
||||
{
|
||||
public SimulationTickContext(float deltaTime, float realDeltaTime, Vector3 playerPosition)
|
||||
{
|
||||
DeltaTime = deltaTime;
|
||||
RealDeltaTime = realDeltaTime;
|
||||
PlayerPosition = playerPosition;
|
||||
}
|
||||
|
||||
public float DeltaTime { get; }
|
||||
public float RealDeltaTime { get; }
|
||||
public Vector3 PlayerPosition { get; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4c57fcf285f488b821c9141a6d0ad09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
using Entity;
|
||||
using GameFramework.Event;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
// Bridges entity show/hide events into simulation state registration.
|
||||
public sealed class EntitySync
|
||||
{
|
||||
private const string EnemyGroupName = "Enemy";
|
||||
private const string DropGroupName = "Drop";
|
||||
private const string BulletGroupName = "Bullet";
|
||||
private const string ProjectileGroupName = "Projectile";
|
||||
private const string EnemyProjectileGroupName = "EnemyProjectile";
|
||||
|
||||
private readonly SimulationWorld _world;
|
||||
private bool _isEventSubscribed;
|
||||
|
||||
public EntitySync(SimulationWorld world)
|
||||
{
|
||||
_world = world;
|
||||
}
|
||||
|
||||
public void OnStart()
|
||||
{
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
eventComponent.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
eventComponent.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_isEventSubscribed = true;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_isEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent != null)
|
||||
{
|
||||
eventComponent.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
eventComponent.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
}
|
||||
|
||||
_isEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is not ShowEntitySuccessEventArgs args) return;
|
||||
if (args.Entity == null) return;
|
||||
if (_world == null) return;
|
||||
|
||||
string groupName = args.Entity.EntityGroup?.Name;
|
||||
if (string.IsNullOrEmpty(groupName)) return;
|
||||
|
||||
if (groupName == EnemyGroupName && args.Entity.Logic is EnemyBase enemy)
|
||||
{
|
||||
_world.RegisterEnemyLifecycle(enemy, args.UserData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupName == DropGroupName && args.Entity.Logic is EntityBase pickupEntity)
|
||||
{
|
||||
_world.RegisterPickupLifecycle(pickupEntity);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((groupName == BulletGroupName || groupName == ProjectileGroupName ||
|
||||
groupName == EnemyProjectileGroupName) &&
|
||||
args.Entity.Logic is EntityBase projectileEntity)
|
||||
{
|
||||
_world.RegisterProjectileLifecycle(projectileEntity, args.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is not HideEntityCompleteEventArgs args) return;
|
||||
if (args.EntityGroup == null) return;
|
||||
if (_world == null) return;
|
||||
|
||||
string groupName = args.EntityGroup.Name;
|
||||
if (groupName == EnemyGroupName)
|
||||
{
|
||||
_world.UnregisterEnemyLifecycle(args.EntityId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupName == DropGroupName)
|
||||
{
|
||||
_world.UnregisterPickupLifecycle(args.EntityId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupName == BulletGroupName || groupName == ProjectileGroupName ||
|
||||
groupName == EnemyProjectileGroupName)
|
||||
{
|
||||
_world.UnregisterProjectileLifecycle(args.EntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54d72e7241f04623bcfddfd096086dbb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
using Components;
|
||||
using Entity;
|
||||
using Entity.EntityData;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Entity To Sim Data
|
||||
|
||||
private static EnemySimData CreateEnemyInitialSimData(EnemyBase enemy, EnemyData enemyData)
|
||||
{
|
||||
Transform enemyTransform = enemy.CachedTransform;
|
||||
MovementComponent movementComponent = enemy.GetComponent<MovementComponent>();
|
||||
|
||||
float speed = 0f;
|
||||
if (enemyData != null)
|
||||
{
|
||||
speed = enemyData.SpeedBase;
|
||||
}
|
||||
else if (movementComponent != null)
|
||||
{
|
||||
speed = movementComponent.Speed;
|
||||
}
|
||||
|
||||
float attackRange = enemy.AttackRange > 0f
|
||||
? enemy.AttackRange
|
||||
: DefaultAttackRange;
|
||||
|
||||
return new EnemySimData
|
||||
{
|
||||
EntityId = enemy.Id,
|
||||
Position = enemyTransform.position,
|
||||
Forward = enemyTransform.forward,
|
||||
Rotation = enemyTransform.rotation,
|
||||
Speed = speed,
|
||||
AttackRange = attackRange,
|
||||
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
|
||||
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
|
||||
TargetType = 0,
|
||||
State = EnemyStateIdle
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity, object userData)
|
||||
{
|
||||
Vector3 forward = projectileEntity.CachedTransform.forward;
|
||||
int ownerEntityId = 0;
|
||||
Vector3 velocity = Vector3.zero;
|
||||
float speed = 0f;
|
||||
float lifeTime = 0f;
|
||||
|
||||
if (userData is EnemyProjectileData enemyProjectileData)
|
||||
{
|
||||
ownerEntityId = enemyProjectileData.OwnerEntityId;
|
||||
|
||||
Vector3 direction = enemyProjectileData.Direction;
|
||||
direction.y = 0f;
|
||||
if (direction.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
direction.Normalize();
|
||||
forward = direction;
|
||||
}
|
||||
else if (forward.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
forward = forward.normalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
forward = Vector3.forward;
|
||||
}
|
||||
|
||||
speed = Mathf.Max(0f, enemyProjectileData.Speed);
|
||||
velocity = forward * speed;
|
||||
lifeTime = Mathf.Max(0f, enemyProjectileData.LifeTime);
|
||||
}
|
||||
|
||||
return new ProjectileSimData
|
||||
{
|
||||
EntityId = projectileEntity.Id,
|
||||
OwnerEntityId = ownerEntityId,
|
||||
Position = projectileEntity.CachedTransform.position,
|
||||
Forward = forward,
|
||||
Velocity = velocity,
|
||||
Speed = speed,
|
||||
LifeTime = lifeTime,
|
||||
Age = 0f,
|
||||
Active = true,
|
||||
RemainingLifetime = lifeTime,
|
||||
State = ProjectileStateActive
|
||||
};
|
||||
}
|
||||
|
||||
private static PickupSimData CreatePickupInitialSimData(EntityBase pickupEntity)
|
||||
{
|
||||
return new PickupSimData
|
||||
{
|
||||
EntityId = pickupEntity.Id,
|
||||
Position = pickupEntity.CachedTransform.position,
|
||||
PickupRadius = 0.35f,
|
||||
State = 0
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user