2024-11-01 16:55:46 +08:00

677 lines
20 KiB
C#

using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using UnityEditor;
public class Boids : MonoBehaviour
{
private static Boids _instance;
public static Boids Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<Boids>();
}
return _instance;
}
//set => _instance = value;
}
[Header("System Settings")]
public int boidsPerTeam = 256;
int maxBoids;
public ParticleSystem system;
private ParticleSystemRenderer systemRenderer;
public ParticleSystem explosionSystem;
public ParticleSystem.EmitParams explosionSystem_ep;
public ParticleSystem laserSystem;
public ParticleSystem.EmitParams laserSystem_ep;
public float spawnRate = 2.0f;
public Team[] teams;
[Header("Boid Settings")]
public float boidSpeed = 10f;
public float boidSpeedVariation = 2f;
public float boidTurnSpeed = 0.5f;
public float boidTurnSpeedVariation = 0.1f;
public Transform randomBoid;
private Boid nullBoid;
private int boidIndex = -1;
private int boidSpawnIndex;
private NativeArray<Boid> _boids;
[NonSerialized] public NativeArray<Boid> _boidsAlt;
private NativeArray<ParticleSystem.Particle> _particles;
private JobHandle _boidMovementHandle;
private JobHandle _boidCopyHandle;
private float[] DamageList;
// TODO replace with basic state machine
private bool startFrame = true;
private bool resetNextFrame = false;
private float spawnTicker = 1.0f;
private void OnEnable()
{
nullBoid = new Boid
{
active = false,
id = -1
};
maxBoids = boidsPerTeam * teams.Length;
system.AllocateMeshIndexAttribute();
var main = system.main;
main.maxParticles = maxBoids;
_particles = new NativeArray<ParticleSystem.Particle>(maxBoids, Allocator.Persistent);
system.GetParticles(_particles);
_boids = new NativeArray<Boid>(maxBoids, Allocator.Persistent);
DamageList = new float[_boids.Length];
systemRenderer = system.GetComponent<ParticleSystemRenderer>();
var meshes = new Mesh[teams.Length];
for (int t = 0; t < teams.Length; t++)
{
meshes[t] = teams[t].ship.mesh;
for (int i = 0; i < boidsPerTeam; i++)
{
var index = t * boidsPerTeam + i;
// setup boids
var boid = _boids[index];
boid.Init(index % teams.Length, index);
boid.ResetBoid();
_boids[index] = boid;
// setup particles
var p = _particles[index];
p.SetMeshIndex((int)boid.team);
p.startLifetime = 6f;
p.startColor = teams[boid.team].laserColor;
_particles[index] = p;
}
}
systemRenderer.SetMeshes(meshes);
system.SetParticles(_particles);
_boidsAlt = new NativeArray<Boid>(maxBoids, Allocator.Persistent);
}
private void OnDisable()
{
_boidCopyHandle.Complete();
if (_boids.IsCreated)
_boids.Dispose();
if (_particles.IsCreated)
_particles.Dispose();
if (_boidsAlt.IsCreated)
_boidsAlt.Dispose();
}
private void Update()
{
SpawnTicker();
if (startFrame)
return;
_boidCopyHandle.Complete();
if (resetNextFrame)
{
for (var index = 0; index < _boids.Length; index++)
{
var boid = _boids[index];
boid.ResetBoid();
_boids[index] = boid;
}
boidSpawnIndex = 0;
// reset damage list
for (int i = 0; i < DamageList.Length; i++)
{
DamageList[i] = 0;
}
resetNextFrame = false;
}
SpawnNewShips();
SpawnExplosions();
SpawnLasers();
if (randomBoid)
{
if (boidIndex >= 0 && _boids[boidIndex].active && _boids[boidIndex].team == 0)
{
randomBoid.forward = _boids[boidIndex].velocity;
randomBoid.position = _boids[boidIndex].position;// Vector3.Lerp(randomBoid.position, _boids[boidIndex].position, Time.deltaTime);
}
else
{
boidIndex = UnityEngine.Random.Range(0, _boids.Length);
}
}
system.SetParticles(_particles);
_boidsAlt.CopyFrom(_boids);
// read frames
}
private void LateUpdate()
{
// kick off jobs
var movement = new BoidMovementJob()
{
_boids = _boids,
_boidsAlt = _boidsAlt,
deltaTime = Time.deltaTime,
playerBoid = boidIndex,
};
var handle = new JobHandle();
_boidMovementHandle = movement.Schedule(_boids.Length, handle);
var copy = new BoidCopyJob()
{
_particles = _particles,
_boids = _boids,
playerBoid = boidIndex,
};
_boidCopyHandle = copy.Schedule(_boids.Length, 32, _boidMovementHandle);
JobHandle.ScheduleBatchedJobs();
if (startFrame)
startFrame = false;
}
public Boid GetBoid(int id)
{
foreach (var boid in _boidsAlt)
{
if (boid.id == id)
{
return boid;
}
}
return nullBoid;
}
public Boid GetClosestBoid(Vector3 positionWS)
{
return GetClosestBoid(positionWS, -1);
}
public Boid GetClosestBoid(Vector3 positionWS, int team)
{
var i = -1;
var dist = float.PositiveInfinity;
for (var index = 0; index < _boidsAlt.Length; index++)
{
var boid = _boidsAlt[index];
if(!boid.active || boid.health <= 0 || team == boid.team) continue;
var boidDist = Vector3.Distance(boid.position, positionWS);
if (!(boidDist < dist)) continue;
dist = boidDist;
i = index;
}
if (i >= 0 && i < _boidsAlt.Length)
{
return _boidsAlt[i];
}
else
{
return nullBoid;
}
}
public void ResetBoids()
{
resetNextFrame = true;
}
public void DamageBoid(int id, float damage)
{
if(DamageList.Length > id && id >= 0)
{
DamageList[id] += damage;
}
}
[BurstCompile]
struct BoidMovementJob : IJobFor
{
public NativeArray<Boid> _boids;
[ReadOnly]
public NativeArray<Boid> _boidsAlt;
[ReadOnly] public float deltaTime;
[ReadOnly] public int playerBoid;
public void Execute(int index)
{
var b = _boids[index];
if (!b.active) return;
float turningSpeed = b.turningSpeed;
if (index == playerBoid)
turningSpeed *= 1.5f;
// target
float3 targetDir;
float targetFacing = 0f;
b.rateOfFire -= deltaTime;
if(b.targetID == -1 || _boidsAlt[b.targetID].active == false)
{
b.targetID = -1;
b.targetAge = 0;
targetDir = math.normalize(b.destination - b.position);
}
else
{
targetDir = math.normalize(_boidsAlt[b.targetID].position - b.position);
targetFacing = math.dot(targetDir, math.normalize(b.velocity));
targetDir *= 2f;
}
// general boids
float3 cohesionVect = 0;
float3 separationVect = 0;
float3 alignmentVect = 0;
// counts
int separationCount = 0;
int cohesionCount = 0;
for(int i = 0; i < _boidsAlt.Length; i++)
{
if(b.id != _boidsAlt[i].id && _boidsAlt[i].active)
{
float3 distVect = _boidsAlt[i].position - b.position;
float dist = math.length(distVect);
if (b.team == _boidsAlt[i].team) // on team
{
//cohesion
if (dist < 100f)
{
cohesionVect += distVect * 0.1f;
cohesionCount++;
//alignment
alignmentVect += _boidsAlt[i].velocity;
}
}
else
{// if not on team
if(dist < 350f)
{
if((b.targetID == -1 || b.targetAge >= 4200f) && i != playerBoid)
{
b.targetID = i;
b.targetAge = 0;
}
else
{
if (i == b.targetID)
{
var targetBoid = _boids[i];
if (!targetBoid.active)
{
b.targetID = -1;
b.targetAge = 0;
continue;
}
turningSpeed *= 2;
if (targetFacing > 0.85f && b.rateOfFire <= 0)
{
targetBoid.health -= b.damage;
b.shooting = true;
b.rateOfFire = 0.8f;
}
if (targetBoid.health <= 0f)
{
targetBoid.active = false;
}
_boids[i] = targetBoid;
}
}
}
}
//separation
if(dist <= 50f && dist > 0){
separationVect -= distVect / (dist * 0.1f);
separationCount++;
}
}
}
if (cohesionCount > 0)
{
cohesionVect /= cohesionCount;
alignmentVect /= cohesionCount;
}
if(separationCount > 0)
separationVect /= separationCount;
var vec = float3.zero;
vec += cohesionVect * 0.01f;
vec += separationVect * 0.5f;
vec += alignmentVect * 0.1f;
vec += targetDir;
vec += b.velocity;
var vel = math.normalizesafe(vec + math.EPSILON);
vel = RotateTowards(b.velocity, vel, math.radians(100f * deltaTime), 0f); // clamp the velocity
b.velocity = math.lerp(b.velocity, math.lerp(b.smoothVel, vel, 0.5f), 0.5f); // smooth the velocity
b.position += b.velocity * b.speed * deltaTime; // scale to speed
b.smoothVel = math.lerp(b.smoothVel, vel, 0.1f);
b.targetAge++;
_boids[index] = b;
}
float3 RotateTowards(float3 start, float3 end, float maxAngle, float maxMagnitude)
{
var startMag = math.length(start);
var endMag = math.length(end);
var dot = math.dot(start, end);
if (dot > 1f - math.EPSILON) // direction almost the same
{
return end;
}
else // normal case
{
var angle = math.acos(dot);
var axis = math.normalize(math.cross(start, end));
var matrix = float3x3.AxisAngle(axis, math.min(angle, maxAngle));
float3 rotated = math.mul(matrix, start);
rotated *= ClampedMove(startMag, endMag, maxMagnitude);
return math.normalize(rotated);
}
}
static float ClampedMove(float start, float end, float clampedDelta)
{
var delta = end - start;
if (delta > 0.0F)
return start + math.min(delta, clampedDelta);
else
return start - math.min(-delta, clampedDelta);
}
static float3 Slerp(float3 start, float3 end, float percent)
{
float dotP = math.dot(start, end);
dotP = math.clamp(dotP, -1.0f, 1.0f);
float theta = math.acos(dotP)*percent;
float3 RelativeVec = math.normalizesafe(end - start*dotP);
return ((start*math.cos(theta)) + (RelativeVec*math.sin(theta)));
}
}
[BurstCompile]
struct BoidCopyJob : IJobParallelFor
{
public NativeArray<ParticleSystem.Particle> _particles;
[ReadOnly]
public NativeArray<Boid> _boids;
[ReadOnly] public int playerBoid;
public void Execute(int index)
{
var p = _particles[index];
var b = _boids[index];
if (b.active)
{
//setup alive boid
p.velocity = math.normalizesafe(b.position - (float3)p.position);
p.position = b.position;
p.remainingLifetime = math.clamp(p.remainingLifetime + 1f, 0.1f, 10f);
p.rotation = 0f;
p.startSize3D = index != playerBoid ? Vector3.one : Vector3.zero;
p.startSize = math.clamp(p.startSize + 0.1f, 0.0f, 1f);
}
else
{
//null boid
p.position = Vector3.zero;
p.remainingLifetime = -10f;
p.rotation += 0.1f;
p.startSize3D = Vector3.zero;
p.startSize = 0f;
}
_particles[index] = p;
}
}
private void SpawnLasers()
{
// do the lasers
for (int i = 0; i < _boids.Length; i++)
{
var b = _boids[i];
if (b.shooting && b.targetID != -1 && _boids[b.targetID].active)
{
laserSystem_ep.position = b.position;
var vector = _boids[b.targetID].position - b.position;
var distance = math.length(vector);
var dir = math.normalize(vector);
laserSystem_ep.velocity = dir * 1200f;
laserSystem_ep.startLifetime = distance / 1200f;
laserSystem_ep.startColor = teams[b.team].laserColor;
laserSystem.Emit(laserSystem_ep, 1);
b.shooting = false;
}
_boids[i] = b;
}
}
private void SpawnExplosions()
{
// spawn explosions at each death
for (var i = 0; i < _boids.Length; i++)
{
var boid = _boids[i];
boid.health -= DamageList[boid.id];
DamageList[boid.id] = 0f;
if (boid.health <= 0f)
{
explosionSystem_ep.position = boid.position;
explosionSystem_ep.velocity = boid.velocity * 0.1f;
explosionSystem.Emit(explosionSystem_ep, 1);
boid.health = 100f;
boid.position = teams[boid.team].spawnPoints[UnityEngine.Random.Range(0, teams[boid.team].spawnPoints.Length)].position;
}
_boids[i] = boid;
}
}
private void SpawnNewShips()
{
if (spawnTicker > 0f) return;
while (_boids[boidSpawnIndex].active)
{
boidSpawnIndex = (int)Mathf.Repeat(boidSpawnIndex + 1, _boids.Length);
}
var boid = _boids[boidSpawnIndex];
var point = teams[boid.team].spawnPoints[UnityEngine.Random.Range(0, teams[boid.team].spawnPoints.Length)];
if (point.gameObject.activeInHierarchy)
{
boid.Spawn(point.position, point.forward);
_boids[boidSpawnIndex] = boid;
boidSpawnIndex = 0;
}
ResetTicker();
}
private void SpawnTicker()
{
if (spawnTicker > 0f)
spawnTicker -= Time.deltaTime;
else
{
ResetTicker();
}
}
private void ResetTicker()
{
spawnTicker = Mathf.Max(0.05f, spawnRate);
}
public struct Boid
{
public float3 position;
public float3 velocity;
public float3 smoothVel;
public float3 destination;
// states
public bool active;
public bool shooting;
public float health;
public float damage;
public float rateOfFire;
public float speed;
public float turningSpeed;
public int targetID;
public int targetAge;
public int team;
public int id;
public void Init(int team, int id)
{
position = float3.zero;
velocity = float3.zero;
smoothVel = float3.zero;
destination = float3.zero;
active = false;
shooting = false;
health = 100f;
damage = 22f;
rateOfFire = 0.5f;
speed = 10f;
turningSpeed = 0.5f;
targetID = -1;
targetAge = 1;
this.team = team;
this.id = id;
}
public void ResetBoid()
{
Init(team, id);
}
public void Spawn(Vector3 position, Vector3 direction)
{
this.position = position + UnityEngine.Random.insideUnitSphere;
var dir = direction;
velocity = dir * speed;
active = true;
}
}
[Serializable]
public class Team
{
public Color shipColor;
public Color laserColor;
public Transform[] spawnPoints;
public Ship ship;
}
[Serializable]
public class Ship
{
public Mesh mesh;
}
#if UNITY_EDITOR
private GUIStyle style;
private void OnDrawGizmos()
{
foreach (var boid in _boidsAlt)
{
Gizmos.color = teams[boid.team].shipColor * (boid.active ? 1f : 0.1f);
Gizmos.DrawWireSphere(boid.position, boid.id == boidIndex ? 20f : 10f);
if (boid.active)
{
Gizmos.color = Color.gray * 0.25f;
Gizmos.DrawLine(boid.position, boid.destination);
if (boid.targetID >= 0)
{
var target = _boidsAlt[boid.targetID];
Gizmos.color = Color.red * 0.25f;
Gizmos.DrawLine(boid.position, target.position);
Gizmos.color = Color.red;
Gizmos.DrawLine(boid.position,
(Vector3) boid.position + Vector3.Normalize(target.position - boid.position) * 25f);
}
var text = $"boid:{boid.id}\n" +
$"HP:{boid.health}";
style ??= new GUIStyle();
style.normal.textColor = teams[boid.team].shipColor;
Handles.Label(boid.position, text, style);
}
}
}
#endif
}