This commit is contained in:
2026-06-09 09:18:17 +07:00
parent 3578a2750c
commit 71a096556a
5777 changed files with 6675 additions and 13 deletions

View File

@@ -1,119 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using Opsive.Shared.Game;
using Opsive.UltimateCharacterController.Items.Actions;
using UnityEngine;
/// <summary>
/// The MagicParticle will perform a MagicItem impact when it collides with an object. In order for this to work correctly the ParticleSystem must
/// have collisions enabled and the "Send Collision Event" parameter enabled. See this page for more information:
/// https://docs.unity3d.com/Manual/PartSysCollisionModule.html.
/// </summary>
public class MagicParticle : MonoBehaviour
{
[Tooltip("Can the particle collide with the originator?")]
[SerializeField] protected bool m_CanCollideWithOriginator = false;
private GameObject m_GameObject;
private Transform m_Transform;
private MagicItem m_MagicItem;
private uint m_CastID;
/// <summary>
/// Initialize the default values.
/// </summary>
private void Awake()
{
m_GameObject = gameObject;
m_Transform = transform;
var particleSystem = GetComponent<ParticleSystem>();
if (particleSystem == null) {
Debug.LogError($"Error: The MagicProjectile {m_GameObject.name} does not have a ParticleSystem attached.");
return;
}
if (!particleSystem.collision.enabled) {
Debug.LogError($"Error: The collision module on the MagicProjectile {m_GameObject.name} is disabled. This should be enabled in order to receive collision events.");
return;
}
if (!particleSystem.collision.sendCollisionMessages) {
Debug.LogError($"Error: Send Collision Messages on the the MagicProjectile {m_GameObject.name} is disabled. This should be enabled in order to receive collision events.");
return;
}
}
/// <summary>
/// Initializes the particle to the specified MagicItem.
/// </summary>
/// <param name="magicItem">The MagicItem that casted the particle.</param>
/// <param name="castID">The ID of the MagicItem cast.</param>
public void Initialize(MagicItem magicItem, uint castID)
{
m_MagicItem = magicItem;
m_CastID = castID;
}
/// <summary>
/// A particle has collided with another object.
/// </summary>
/// <param name="other">The object that the particle collided with.</param>
public void OnParticleCollision(GameObject other)
{
// If the transform is null the particle hasn't been initialized yet.
if (m_Transform == null) {
return;
}
// Prevent the particle from colliding with the originator.
if (!m_CanCollideWithOriginator) {
var characterLocomotion = other.GetCachedComponent<Character.UltimateCharacterLocomotion>();
if (characterLocomotion != null && m_MagicItem.Character == characterLocomotion.gameObject) {
return;
}
}
// PerformImpact requires a RaycastHit.
var colliders = other.GetCachedComponents<Collider>();
if (colliders == null) {
return;
}
for (int i = 0; i < colliders.Length; ++i) {
if (colliders[i].isTrigger) {
continue;
}
Vector3 closestPoint;
if (colliders[i] is BoxCollider || colliders[i] is SphereCollider || colliders[i] is CapsuleCollider || (colliders[i] is MeshCollider && (colliders[i] as MeshCollider).convex)) {
closestPoint = colliders[i].ClosestPoint(m_Transform.position);
} else {
closestPoint = m_Transform.position;
}
var direction = other.transform.position - closestPoint;
if (Physics.Raycast(closestPoint - direction.normalized * 0.1f, direction.normalized, out var hit, direction.magnitude + 0.1f, 1 << other.layer)) {
m_MagicItem.PerformImpact(m_CastID, m_GameObject, other, hit);
break;
}
}
}
/// <summary>
/// The particle has been disabled.
/// </summary>
private void OnDisable()
{
// All of the impact actions should be reset for the particle spawn id.
if (m_MagicItem != null && m_MagicItem.ImpactActions != null) {
for (int i = 0; i < m_MagicItem.ImpactActions.Length; ++i) {
m_MagicItem.ImpactActions[i].Reset(m_CastID);
}
}
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: f6db731486d10934cbfa28dc65e38c1a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,106 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using Opsive.Shared.Game;
using Opsive.UltimateCharacterController.Items.Actions;
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
using Opsive.UltimateCharacterController.Networking.Game;
#endif
using Opsive.UltimateCharacterController.Objects;
using UnityEngine;
/// <summary>
/// The ParticleProjectile extends TrajectoryObject and notifies the MagicItem when the object has collided with another object.
/// TrajectoryObject.CollisionMode should be set to Ignore for the projectile to pass through the object.
/// </summary>
public class MagicProjectile : TrajectoryObject
{
[Tooltip("Should the projectile be destroyed when there's a collision?")]
[SerializeField] protected bool m_DestroyOnCollision;
[Tooltip("Should the projectile be destroyed after the particle has stopped emitting?")]
[SerializeField] protected bool m_WaitForParticleStop;
public bool DestroyOnCollision { get { return m_DestroyOnCollision; } set { m_DestroyOnCollision = value; } }
public bool WaitForParticleStop { get { return m_WaitForParticleStop; } set { m_WaitForParticleStop = value; } }
protected MagicItem m_MagicItem;
protected uint m_CastID;
/// <summary>
/// Initializes the object with the specified velocity and torque.
/// </summary>
/// <param name="velocity">The starting velocity.</param>
/// <param name="torque">The starting torque.</param>
/// <param name="originator">The object that instantiated the trajectory object.</param>
/// <param name="magicItem">The MagicItem that created the projectile.</param>
/// <param name="castID">The ID of the cast.</param>
public void Initialize(Vector3 velocity, Vector3 torque, GameObject originator, MagicItem magicItem, uint castID)
{
m_MagicItem = magicItem;
m_CastID = castID;
Initialize(velocity, torque, originator);
if (m_Collider != null) {
m_Collider.enabled = false;
}
}
/// <summary>
/// The object has collided with another object.
/// </summary>
/// <param name="hit">The RaycastHit of the object. Can be null.</param>
protected override void OnCollision(RaycastHit? hit)
{
base.OnCollision(hit);
if (!hit.HasValue) {
return;
}
m_MagicItem.PerformImpact(m_CastID, m_GameObject, hit.Value.transform.gameObject, hit.Value);
// Destroys the projectile when it has collided with an object.
if (m_DestroyOnCollision) {
// The projectile can wait for any particles to stop emitting.
var immediateDestroy = !m_WaitForParticleStop;
if (!immediateDestroy) {
var particleSystem = m_GameObject.GetCachedComponent<ParticleSystem>();
if (particleSystem != null) {
particleSystem.Stop(true, ParticleSystemStopBehavior.StopEmitting);
Scheduler.Schedule(particleSystem.main.duration, ReturnToObjectPool);
immediateDestroy = false;
}
}
if (immediateDestroy) {
ReturnToObjectPool();
} else {
// The projectile is waiting on the particles to be destroyed. Stop moving.
Stop();
}
}
}
/// <summary>
/// Returns the projectile back to the object pool.
/// </summary>
private void ReturnToObjectPool()
{
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
if (NetworkObjectPool.IsNetworkActive()) {
// The object may have already been destroyed over the network.
if (!m_GameObject.activeSelf) {
return;
}
NetworkObjectPool.Destroy(m_GameObject);
return;
}
#endif
ObjectPool.Destroy(m_GameObject);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 9a93ab405fbc6e54db16c521de2c6cc5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,21 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using UnityEngine;
/// <summary>
/// Specifies an offset for the pivot position.
/// </summary>
public class PivotOffset : MonoBehaviour
{
[Tooltip("The pivot offset.")]
[SerializeField] protected Vector3 m_Offset;
public Vector3 Offset { get { return m_Offset; } set { m_Offset = value; } }
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 7a16c458321536b43b2e57119a8cb244
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,17 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using UnityEngine;
/// <summary>
/// Specifies that the attached object should cause recoil when hit with a MeleeWeapon.
/// </summary>
public class RecoilObject : MonoBehaviour
{
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: fa25a7ec2cbec8a40988d4328115db41
timeCreated: 1532978512
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,89 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using Opsive.Shared.Game;
using UnityEngine;
/// <summary>
/// Represents a shell casing which uses the trajectory object for kinematic shell movement.
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class Shell : TrajectoryObject
{
[Tooltip("Time to live in seconds before the shell is removed.")]
[SerializeField] protected float m_Lifespan = 10;
[Tooltip("Chance of shell not being removed after settling on the ground.")]
[Range(0, 1)] [SerializeField] protected float m_Persistence = 1;
private float m_RemoveTime;
private Vector3 m_StartScale;
/// <summary>
/// Initialize the default values.
/// </summary>
protected override void Awake()
{
base.Awake();
m_StartScale = transform.localScale;
// The Rigidbody is only used to notify Unity that the object isn't static. The Rigidbody doesn't control any movement.
var rigidbody = GetComponent<Rigidbody>();
rigidbody.mass = m_Mass;
rigidbody.isKinematic = true;
rigidbody.constraints = RigidbodyConstraints.FreezeAll;
}
/// <summary>
/// The shell has been spawned - reset the timing and component values.
/// </summary>
protected override void OnEnable()
{
base.OnEnable();
m_RemoveTime = Time.time + m_Lifespan;
m_Transform.localScale = m_StartScale;
if (m_Collider != null) {
m_Collider.enabled = true;
}
}
/// <summary>
/// Move and rotate the object according to a parabolic trajectory.
/// </summary>
protected override void FixedUpdate()
{
base.FixedUpdate();
if (Time.time > m_RemoveTime) { // The shell should be removed.
m_Transform.localScale = Vector3.Lerp(m_Transform.localScale, Vector3.zero, Utility.TimeUtility.FramerateDeltaTime * 0.2f);
if (Time.time > m_RemoveTime + 0.5f) {
ObjectPool.Destroy(m_GameObject);
}
}
}
/// <summary>
/// The object has collided with another object.
/// </summary>
/// <param name="hit">The RaycastHit of the object. Can be null.</param>
protected override void OnCollision(RaycastHit? hit)
{
base.OnCollision(hit);
if (m_Velocity.sqrMagnitude > 4) { // Hard bounce.
// Apply more random rotation velocity to make the shell behave a bit unpredictably on a hard bounce (similar to real brass shell behavior).
AddTorque(Random.rotation.eulerAngles * 0.15f * (Random.value > 0.5f ? 1 : -1));
} else if (Random.value > m_Persistence) { // Soft bounce.
// Remove the shell after half a second on a soft bounce.
m_RemoveTime = Time.time + 0.5f;
}
}
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: ec2d29bda368ada499ad37e3d0781339
timeCreated: 1505688289
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,63 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using Opsive.Shared.Events;
using Opsive.UltimateCharacterController.Items.Actions;
using UnityEngine;
/// <summary>
/// The ShieldCollider component specifies the object that acts as a collider for the shield.
/// </summary>
public class ShieldCollider : MonoBehaviour
{
[Tooltip("A reference to the Shield item action.")]
[SerializeField] protected Shield m_Shield;
[Tooltip("Is the collider attached to a Shield used for the first person perspective?")]
[HideInInspector] [SerializeField] protected bool m_FirstPersonPerspective;
[Shared.Utility.NonSerialized] public Shield Shield { get { return m_Shield; } set { m_Shield = value; } }
[Shared.Utility.NonSerialized] public bool FirstPersonPerspective { set { m_FirstPersonPerspective = value; } }
private Collider m_Collider;
private GameObject m_Character;
/// <summary>
/// Initialize the default values.
/// </summary>
private void Awake()
{
if (m_Shield == null) {
Debug.LogError("Error: The shield is not assigned. Ensure the shield is created from the Item Manager.", this);
return;
}
m_Collider = GetComponent<Collider>();
m_Collider.enabled = false;
m_Character = m_Shield.gameObject.GetComponentInParent<Character.UltimateCharacterLocomotion>().gameObject;
EventHandler.RegisterEvent<bool>(m_Character, "OnCharacterChangePerspectives", OnChangePerspectives);
}
/// <summary>
/// The camera perspective between first and third person has changed.
/// </summary>
/// <param name="inFirstPerson">Is the camera in a first person view?</param>
private void OnChangePerspectives(bool firstPersonPerspective)
{
// The collider should only be enabled for the corresponding perspective.
m_Collider.enabled = m_FirstPersonPerspective == firstPersonPerspective;
}
/// <summary>
/// The object has been destroyed.
/// </summary>
private void OnDestroy()
{
EventHandler.UnregisterEvent<bool>(m_Character, "OnCharacterChangePerspectives", OnChangePerspectives);
}
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 16cb3bccd7b264c46ba4804f977c18b9
timeCreated: 1534209342
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,124 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using Opsive.Shared.Events;
using Opsive.Shared.Game;
using Opsive.UltimateCharacterController.Character;
using Opsive.UltimateCharacterController.Game;
using Opsive.UltimateCharacterController.Items;
using Opsive.UltimateCharacterController.Items.Actions.PerspectiveProperties;
using UnityEngine;
/// <summary>
/// The Smoke component is attached to a GameObject with the a ParticleSystem attached representing smoke.
/// </summary>
public class Smoke : MonoBehaviour
{
private GameObject m_GameObject;
private Transform m_Transform;
private Item m_Item;
private int m_ItemActionID;
private ParticleSystem[] m_Particles;
private ParticleSystemSimulationSpace[] m_SimulationSpace;
private GameObject m_Character;
private int m_StartLayer;
/// <summary>
/// Initialize the default values.
/// </summary>
private void Awake()
{
m_GameObject = gameObject;
m_Transform = transform;
m_Particles = GetComponentsInChildren<ParticleSystem>();
m_SimulationSpace = new ParticleSystemSimulationSpace[m_Particles.Length];
m_StartLayer = m_GameObject.layer;
}
/// <summary>
/// A weapon has been fired and the smoke needs to show.
/// </summary>
/// <param name="item">The item that the muzzle flash is attached to.</param>
/// <param name="itemActionID">The ID which corresponds to the ItemAction that spawned the smoke.</param>
/// <param name="characterLocomotion">The character that the smoke is attached to.</param>
public void Show(Item item, int itemActionID, UltimateCharacterLocomotion characterLocomotion)
{
m_Character = characterLocomotion.gameObject;
EventHandler.RegisterEvent<bool>(m_Character, "OnCharacterChangePerspectives", OnChangePerspectives);
m_Item = item;
m_ItemActionID = itemActionID;
m_GameObject.layer = characterLocomotion.FirstPersonPerspective ? LayerManager.Overlay : m_StartLayer;
// Disable the object after the particles are done playing.
float maxLifeTime = 0;
for (int i = 0; i < m_Particles.Length; ++i) {
m_Particles[i].Play();
var lifeTime = 0f;
if ((lifeTime = m_Particles[i].main.startLifetime.Evaluate(0)) > maxLifeTime) {
maxLifeTime = lifeTime;
}
}
Scheduler.Schedule(maxLifeTime, DestroySelf);
}
/// <summary>
/// Place itself back in the ObjectPool.
/// </summary>
public void DestroySelf()
{
ObjectPool.Destroy(m_GameObject);
}
/// <summary>
/// The character perspective between first and third person has changed.
/// </summary>
/// <param name="firstPersonPerspective">Is the character in a first person perspective?</param>
private void OnChangePerspectives(bool firstPersonPerspective)
{
// All of the particles should be set to local space so they'll change correctly when switching perspective.
for (int i = 0; i < m_Particles.Length; ++i) {
m_SimulationSpace[i] = m_Particles[i].main.simulationSpace;
var mainParticle = m_Particles[i].main;
mainParticle.simulationSpace = ParticleSystemSimulationSpace.Local;
}
// When switching locations the local position and rotation should remain the same.
var localPosition = m_Transform.localPosition;
var localRotation = m_Transform.rotation;
var itemAction = m_Item.ItemActions[m_ItemActionID];
var perspectiveProperties = (firstPersonPerspective ? itemAction.FirstPersonPerspectiveProperties : itemAction.ThirdPersonPerspectiveProperties);
var smokeLocation = (perspectiveProperties as IShootableWeaponPerspectiveProperties).SmokeLocation;
m_Transform.parent = smokeLocation;
m_Transform.localPosition = localPosition;
m_Transform.rotation = localRotation;
// Switch the particle simulation space back to the previous value.
for (int i = 0; i < m_Particles.Length; ++i) {
var mainParticle = m_Particles[i].main;
mainParticle.simulationSpace = m_SimulationSpace[i];
}
m_GameObject.layer = firstPersonPerspective ? LayerManager.Overlay : m_StartLayer;
}
/// <summary>
/// The object has been disabled.
/// </summary>
private void OnDisable()
{
if (m_Character != null) {
EventHandler.UnregisterEvent<bool>(m_Character, "OnCharacterChangePerspectives", OnChangePerspectives);
}
m_Character = null;
}
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 2c09be7c342cf2642a12738bb38baaf5
timeCreated: 1505602291
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,53 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using Opsive.Shared.Game;
using UnityEngine;
/// <summary>
/// The tracer will show a Line Renderer from the hitscan fire point to the hit point.
/// </summary>
public class Tracer : MonoBehaviour
{
[Tooltip("The amount of time that the tracer is visible for")]
[SerializeField] protected float m_VisibleTime = 0.05f;
// Component references
private Transform m_Transform;
private LineRenderer m_LineRenderer;
/// <summary>
/// Initialize the default values.
/// </summary>
private void Awake()
{
m_Transform = transform;
m_LineRenderer = GetComponent<LineRenderer>();
}
/// <summary>
/// Sets the hit point that the tracer should move to.
/// </summary>
/// <param name="hitPoint">The hit point position.</param>
public virtual void Initialize(Vector3 hitPoint)
{
m_LineRenderer.SetPosition(0, m_Transform.position);
m_LineRenderer.SetPosition(1, hitPoint);
Scheduler.Schedule(m_VisibleTime, DestroyObject);
}
/// <summary>
/// Places the object back in the ObjectPool.
/// </summary>
private void DestroyObject()
{
ObjectPool.Destroy(gameObject);
}
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 27f97a858bca96045a845d9af5c6dba3
timeCreated: 1511455046
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,396 +0,0 @@
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Objects.ItemAssist
{
using Opsive.Shared.Game;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Builds a mesh which can show a trail following a melee weapon. This is typically used when the melee weapon is slashed.
/// </summary>
public class Trail : MonoBehaviour
{
[Tooltip("The minimum distance between the position of the last slice and the current position.")]
[SerializeField] protected float m_MinDistance = 0.01f;
[Tooltip("The vertical length of the trail.")]
[SerializeField] protected float m_Length = 1f;
[Tooltip("The maximum number of slices within the trail. A larger value will cause the trail to be longer.")]
[SerializeField] protected int m_MaxSliceCount = 50;
[Tooltip("The smoothing value of the curve. A larger value will have a smoother curve compared to a smaller value.")]
[SerializeField] protected int m_CurveSmoothness = 10;
[Tooltip("The steepness of the curve. A value closer to 1 will increase the steepness.")]
[Range(0, 1)] [SerializeField] protected float m_CurveSteepness = 0.5f;
[Tooltip("The start color of the trail, near the melee weapon object.")]
[SerializeField] protected Color m_StartColor = Color.white;
[Tooltip("The end color of the trail.")]
[SerializeField] protected Color m_EndColor = Color.white;
[Tooltip("The amount of time the slice should be visible.")]
[SerializeField] protected float m_VisibilityTime = 1;
private GameObject m_GameObject;
private Transform m_Transform;
private Mesh m_Mesh;
private float m_MinDistanceSquared;
private TrailSlice[] m_TrailSlices = new TrailSlice[4];
private int m_TrailSlicesIndex = -1;
private int m_TrailSlicesCount;
private TrailSlice[] m_SmoothedTrailSlices;
private int m_SmoothedTrailSlicesIndex = -1;
private int m_SmoothedTrailSlicesCount;
private int m_SmoothedTrailSlicesPrevCount;
private int m_SmoothedTrailSlicesPrevIndex;
private List<Vector3> m_Vertices;
private List<Vector2> m_UVs;
private List<Color> m_Colors;
private List<int> m_Triangles;
private bool m_GenerateSlices;
/// <summary>
/// A small container class for each object that represents a slice from the melee trail.
/// </summary>
private struct TrailSlice
{
private Vector3 m_Point;
private Vector3 m_Up;
private float m_Time;
public Vector3 Point { get { return m_Point; } }
public Vector3 Up { get { return m_Up; } }
public float Time { get { return m_Time; } }
/// <summary>
/// Initializes the slice.
/// </summary>
/// <param name="point">The position of the slice.</param>
/// <param name="up">The up direction of the slice.</param>
public void Initialize(Vector3 point, Vector3 up)
{
m_Point = point;
m_Up = up;
m_Time = UnityEngine.Time.time;
}
}
/// <summary>
/// Initializes the trail.
/// </summary>
private void Awake()
{
m_GameObject = gameObject;
m_Transform = transform;
var meshFilter = GetComponent<MeshFilter>();
if (meshFilter == null) {
Debug.LogError("Error: Unable to find the MeshFilter component. Enssure the Trail object has been created through the Object Manager.");
return;
}
m_Mesh = meshFilter.mesh;
m_MinDistanceSquared = m_MinDistance * m_MinDistance;
var count = m_MaxSliceCount * m_CurveSmoothness;
m_SmoothedTrailSlices = new TrailSlice[count];
m_Vertices = new List<Vector3>(count * 2);
m_UVs = new List<Vector2>(count * 2);
m_Colors = new List<Color>(count * 2);
m_Triangles = new List<int>((count - 1) * 6); // 3 indices per triangle, 2 triangles per slice.
}
/// <summary>
/// Start to generate the trail slices.
/// </summary>
private void OnEnable()
{
m_GenerateSlices = true;
}
/// <summary>
/// Samples the position of the melee object.
/// </summary>
private void FixedUpdate()
{
if (m_GenerateSlices) {
SampleTrail();
}
}
/// <summary>
/// Displays the trail.
/// </summary>
private void LateUpdate()
{
RemoveOldSlices();
BuildMesh();
}
/// <summary>
/// Stores a new sample of the trail slice.
/// </summary>
private void SampleTrail()
{
// Add a new slice if the last sample position is too far away.
if (m_TrailSlicesCount == 0 || (m_TrailSlices[m_TrailSlicesIndex].Point - m_Transform.position).sqrMagnitude > m_MinDistanceSquared) {
// Revert the trail slice index and smoothed index/count values so the extra slice can be removed. A more accurate slice value will replace the prediction.
if (m_TrailSlicesCount > 3) {
m_TrailSlicesIndex = m_TrailSlicesIndex - 1;
if (m_TrailSlicesIndex < 0) {
m_TrailSlicesIndex += m_TrailSlicesCount;
}
m_SmoothedTrailSlicesIndex = m_SmoothedTrailSlicesPrevIndex;
m_SmoothedTrailSlicesCount = m_SmoothedTrailSlicesPrevCount;
}
// Add the new slice at the current position.
AddTrailSlice(m_Transform.position, m_Transform.up);
// A catmull-rom curve smooths the middle two verticies rather then all four vertices. Add one more slice near the beginning of the trail so the start of the
// curve will intersect with the melee object.
if (m_TrailSlicesCount > 3) {
var prevIndex = m_TrailSlicesIndex - 1;
if (prevIndex < 0) {
prevIndex = m_TrailSlicesCount - 1;
}
var prevTrailSlice = m_TrailSlices[prevIndex];
var trailSlice = m_TrailSlices[m_TrailSlicesIndex];
// Remember the previous smoothed values so the extra slice can be removed.
m_SmoothedTrailSlicesPrevIndex = m_SmoothedTrailSlicesIndex;
m_SmoothedTrailSlicesPrevCount = m_SmoothedTrailSlicesCount;
// The new slice should be in the previous to the current slice position. This value probably won't be correct the next frame unless
// the object is moving in a linear path but it is a good prediction.
AddTrailSlice(trailSlice.Point + (trailSlice.Point - prevTrailSlice.Point).normalized, (trailSlice.Up + prevTrailSlice.Up) / 2);
}
}
}
/// <summary>
/// Adds the point and up vertex to the trail slices array. The values will also be smoothed.
/// </summary>
/// <param name="point">The point of the slice.</param>
/// <param name="up">The up direction of the slice.</param>
private void AddTrailSlice(Vector3 point, Vector3 up)
{
// Catmull-rom curves do not like repeated points.
if (m_TrailSlicesCount > 0 && m_TrailSlices[m_TrailSlicesIndex].Point == point) {
return;
}
m_TrailSlicesIndex = (m_TrailSlicesIndex + 1) % m_TrailSlices.Length;
m_TrailSlices[m_TrailSlicesIndex].Initialize(point, up);
if (m_TrailSlicesIndex + 1 > m_TrailSlicesCount) {
m_TrailSlicesCount++;
}
SmoothTrailSlice();
}
/// <summary>
/// Smooths the trail slices with a catmull-rom curve.
/// </summary>
private void SmoothTrailSlice()
{
// A catmull-rom curve requires at least four points.
if (m_TrailSlicesCount < 4) {
return;
}
// A fixed size array is used to store the vertex values. The starting index may not be at the beginning of the array.
var startIndex = m_TrailSlicesIndex - m_TrailSlicesCount + 1;
if (startIndex < 0) {
startIndex = m_TrailSlices.Length + startIndex;
}
// Determine a smoothed value for both the point and up vertex.
var p0 = m_TrailSlices[startIndex].Point;
var p1 = m_TrailSlices[(startIndex + 1) % 4].Point;
var p2 = m_TrailSlices[(startIndex + 2) % 4].Point;
var p3 = m_TrailSlices[(startIndex + 3) % 4].Point;
var u0 = m_TrailSlices[startIndex].Up;
var u1 = m_TrailSlices[(startIndex + 1) % 4].Up;
var u2 = m_TrailSlices[(startIndex + 2) % 4].Up;
var u3 = m_TrailSlices[(startIndex + 3) % 4].Up;
var t1 = CentripetralCatmullRomTime(0, p0, p1);
var t2 = CentripetralCatmullRomTime(t1, p1, p2);
var t3 = CentripetralCatmullRomTime(t2, p2, p3);
// Iterate based on the number of sample values.
var iterAmount = ((t2 - t1) / m_CurveSmoothness);
for (float t = t1; t < t2; t += iterAmount) {
var point = CentripetralCatmullRomValue(p0, p1, p2, p3, 0, t1, t2, t3, t);
var up = CentripetralCatmullRomValue(u0, u1, u2, u3, 0, t1, t2, t3, t);
// The value has been determined. Add it to the smoothed array.
m_SmoothedTrailSlicesIndex = (m_SmoothedTrailSlicesIndex + 1) % m_SmoothedTrailSlices.Length;
m_SmoothedTrailSlices[m_SmoothedTrailSlicesIndex].Initialize(point, up);
if (m_SmoothedTrailSlicesIndex + 1 > m_SmoothedTrailSlicesCount) {
m_SmoothedTrailSlicesCount++;
}
}
}
/// <summary>
/// Returns the time of the centripetral catmull-rom curve, defined in https://en.wikipedia.org/wiki/Centripetal_CatmullRom_spline.
/// </summary>
/// <param name="t">The sample time.</param>
/// <param name="v0">The first vertex.</param>
/// <param name="v1">The second vertex.</param>
/// <returns>The time of the centripetral catmull-rom curve.</returns>
private float CentripetralCatmullRomTime(float t, Vector3 v0, Vector3 v1)
{
var a = Mathf.Pow((v1.x - v0.x), 2f) + Mathf.Pow((v1.y - v0.y), 2f) + Mathf.Pow((v1.z - v0.z), 2f);
var b = Mathf.Pow(a, 0.5f);
var c = Mathf.Pow(b, m_CurveSteepness);
return c + t;
}
/// <summary>
/// Returns the vertex of the centripetral catmull-rom curve, defined in https://en.wikipedia.org/wiki/Centripetal_CatmullRom_spline.
/// </summary>
/// <returns>The vertex of the centripetral catmull-rom curve.</returns>
private Vector3 CentripetralCatmullRomValue(Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, float t0, float t1, float t2, float t3, float t)
{
var a1 = (t1 - t) / (t1 - t0) * v0 + (t - t0) / (t1 - t0) * v1;
var a2 = (t2 - t) / (t2 - t1) * v1 + (t - t1) / (t2 - t1) * v2;
var a3 = (t3 - t) / (t3 - t2) * v2 + (t - t2) / (t3 - t2) * v3;
var b1 = (t2 - t) / (t2 - t0) * a1 + (t - t0) / (t2 - t0) * a2;
var b2 = (t3 - t) / (t3 - t1) * a2 + (t - t1) / (t3 - t1) * a3;
return (t2 - t) / (t2 - t1) * b1 + (t - t1) / (t2 - t1) * b2;
}
/// <summary>
/// Removes any slices which have existed for more than the visible time.
/// </summary>
private void RemoveOldSlices()
{
if (m_TrailSlicesCount == 0) {
if (!m_GenerateSlices) {
if (ObjectPool.InstantiatedWithPool(m_GameObject)) {
ObjectPool.Destroy(m_GameObject);
} else {
m_GameObject.SetActive(false);
}
}
return;
}
var startIndex = m_TrailSlicesIndex - m_TrailSlicesCount + 1;
if (startIndex < 0) {
startIndex = m_TrailSlices.Length + startIndex;
}
var count = m_TrailSlicesCount;
for (int i = 0; i < count; ++i) {
var trailSlice = m_TrailSlices[(startIndex + i) % m_TrailSlices.Length];
if (trailSlice.Time + m_VisibilityTime > Time.time) {
break;
}
// The slice has existed for more than the visiblity time - remove it by decreasing the count.
m_TrailSlicesCount--;
}
if (m_SmoothedTrailSlicesCount == 0) {
return;
}
startIndex = m_SmoothedTrailSlicesIndex - m_SmoothedTrailSlicesCount + 1;
if (startIndex < 0) {
startIndex = m_SmoothedTrailSlices.Length + startIndex;
}
count = m_SmoothedTrailSlicesCount;
for (int i = 0; i < count; ++i) {
var trailSlice = m_SmoothedTrailSlices[(startIndex + i) % m_SmoothedTrailSlices.Length];
if (trailSlice.Time + m_VisibilityTime > Time.time) {
break;
}
// The slice has existed for more than the visiblity time - remove it by decreasing the count.
m_SmoothedTrailSlicesCount--;
m_SmoothedTrailSlicesPrevCount--;
}
if (m_TrailSlicesCount == 0 && m_SmoothedTrailSlicesCount == 0) {
m_GenerateSlices = false;
}
}
/// <summary>
/// Creates the mesh from the catmull rom verticies.
/// </summary>
private void BuildMesh()
{
m_Mesh.Clear();
if (m_TrailSlicesCount < 4) {
return;
}
// A fixed size array is used to store the vertex values. The starting index may not be at the beginning of the array.
var startIndex = m_SmoothedTrailSlicesIndex - m_SmoothedTrailSlicesCount + 1;
if (startIndex < 0) {
startIndex = m_SmoothedTrailSlices.Length + startIndex;
}
m_Vertices.Clear();
m_UVs.Clear();
m_Colors.Clear();
m_Triangles.Clear();
for (int i = 0; i < m_SmoothedTrailSlicesCount; ++i) {
var trailSlice = m_SmoothedTrailSlices[(startIndex + i) % m_SmoothedTrailSlices.Length];
// The vertex position is the local position of the slice point. This will allow the trail to stay in the same position while the melee object is moving.
m_Vertices.Add(m_Transform.InverseTransformPoint(trailSlice.Point));
m_Vertices.Add(m_Transform.InverseTransformPoint(trailSlice.Point + trailSlice.Up * m_Length));
// Set the UV value so a texture can be applied to the material.
var u = Mathf.Max(i / (float)(m_SmoothedTrailSlicesCount - 1), 0.01f);
m_UVs.Add(new Vector2(u, 0));
m_UVs.Add(new Vector2(u, 1));
// Optionally lerp between the start and end color.
m_Colors.Add(Color.Lerp(m_EndColor, m_StartColor, u));
// A clear color will fade the trail at the bottom.
m_Colors.Add(Color.clear);
// Map the triangle indices to the vertex element.
if (i < m_SmoothedTrailSlicesCount - 1) {
// First triangle.
m_Triangles.Add((i * 2));
m_Triangles.Add((i * 2) + 1);
m_Triangles.Add((i * 2) + 2);
// Second triangle.
m_Triangles.Add((i * 2) + 2);
m_Triangles.Add((i * 2) + 1);
m_Triangles.Add((i * 2) + 3);
}
}
// Assign the values so the mesh will be displayed on the screen. The list version is used to prevent allocations when the mesh changes size.
m_Mesh.SetVertices(m_Vertices);
m_Mesh.SetUVs(0, m_UVs);
m_Mesh.SetColors(m_Colors);
m_Mesh.SetTriangles(m_Triangles, 0);
}
/// <summary>
/// Stops generating the trail.
/// </summary>
public void StopGeneration()
{
m_Transform.parent = null;
m_GenerateSlices = false;
}
}
}

View File

@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 7f4224c12740b644f82a0f901a35061e
timeCreated: 1531917952
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 370
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: