Oh that’s nice. Some aloud thoughts about the systems, might be a good idea while I’m building them.
Right now the next thing I want to prototype is of course some enemies and then some teamates since I judge those to be crucial for the exploration gameloop. To build some “AI” (or more accurately an “expert system”) I first have to know how the world works. How far down I can abstract it so traverse it.
Unity for example has a tile system they added recently with which you can create tile sets and then brush IDs instead of having to place “objects” manually. Since I’ve not used it before, I have to check out if you can mix different tile sizes or, if it’s one per grid or one per layer… or if I may even want to not use that system altogether.
If all the little sprite tiles are the same size I’m working on a grid. And what is a grid if not a graph… and you know what that means: A* for traversing that graph.
If I want something dynamic that can adjusts to a changing and chaotic environment could use some sort of sensory approach using 2d raycasts, and some weighted steering. That would also take care of the “perfect path” problem (where you calculate a path and it becomes invalid half way through the actor following it)
Since I’m probably gonna go with A* on this here’s a little obstacle avoidance code:
public class ObstacleAvoidanceBehaviour : FlockBehaviour
{
public LayerMask ObstacleMask;
public override Vector3 CalculateMove(Vector3 pos, FlockAgent agent, Flock flock)
{
var up = agent.transform.up;
//// no neighbours, nothing to avoid
//if (!IsHeadingForCollision(pos, up, agent.AgentCollider.radius, flock.ObstacleRadius))
// return Vector2.zero;
Vector3[] rayDirections = BoidHelper.directions;
for (int i = 0; i < rayDirections.Length; i++)
{
var dir = agent.transform.TransformDirection(rayDirections[i]);
var hit = Physics2D.CircleCast(pos, agent.AgentCollider.radius, dir, flock.ObstacleRadius, ObstacleMask);
if (hit == default)
return dir;
}
return up;
}
public bool IsHeadingForCollision(Vector3 pos, Vector3 dir, float radius, float distance)
{
var hit = Physics2D.CircleCast(pos, radius, dir, distance, ObstacleMask);
return hit != default(RaycastHit2D);
}
}
public static class BoidHelper
{
const int numViewDirections = 25;
public static readonly Vector3[] directions;
static BoidHelper()
{
directions = new Vector3[BoidHelper.numViewDirections];
float fullSlice = Mathf.PI * 1.5f;
float midPoint = Mathf.PI / 2 ;
float slicesLeft = 0;
float slicesRight = 0;
bool left = true;
for (int i = 0; i < numViewDirections; i++)
{
float theta;
if (left)
{
theta = midPoint + fullSlice * slicesLeft / numViewDirections;
slicesLeft++;
}
else
{
theta = midPoint - fullSlice * slicesRight / numViewDirections;
slicesRight++;
}
directions[i] = new Vector3(Mathf.Cos(theta), Mathf.Sin(theta), 0).normalized;
left = !left;
}
}
//static BoidHelper()
//{
// directions = new Vector3[BoidHelper.numViewDirections];
// float goldenRatio = (1 + Mathf.Sqrt(5)) / 2;
// float angleIncrement = Mathf.PI * 2 * goldenRatio;
// for (int i = 0; i < numViewDirections; i++)
// {
// float t = (float)i / numViewDirections;
// float inclination = Mathf.Acos(1 - 2 * t);
// float azimuth = angleIncrement * i;
// float x = Mathf.Sin(inclination) * Mathf.Cos(azimuth);
// float y = Mathf.Sin(inclination) * Mathf.Sin(azimuth);
// //float z = Mathf.Cos(inclination);
// directions[i] = new Vector3(x, y, 0).normalized;
// }
//}
}
to stack behaviours you need:
public class CompoundBehaviour : FlockBehaviour
{
public FlockBehaviour[] Behaviours;
public float[] Weights;
public override Vector3 CalculateMove(Vector3 pos, FlockAgent agent, Flock flock)
{
var movement = Vector3.zero;
for (int i = 0; i < Behaviours.Length; i++)
{
var partialMove = Behaviours[i].CalculateMove(pos, agent, flock) * Weights[i];
if (partialMove != Vector3.zero)
{
if (partialMove.sqrMagnitude > Weights[i] * Weights[i])
{
partialMove.Normalize();
partialMove *= Weights[i];
}
}
movement += partialMove;
}
return movement;
}
}
then some cohesion and alignment for a basic boid system:
public class SteeredCohesionBehaviour : FlockBehaviour
{
Vector3 currentVelocity;
public float AgentSmoothTime = 0.5f;
public override Vector3 CalculateMove(Vector3 pos, FlockAgent agent, Flock flock)
{
// no neighbours, no center to move towards
if (agent.Context.Count == 0)
return Vector3.zero;
// get neighbours' center of gravity
var centerOfGravity = Vector3.zero;
agent.Context.ForEach(p => centerOfGravity += p.position);
centerOfGravity /= agent.Context.Count;
// make it relative to agent position
centerOfGravity -= pos;
centerOfGravity = Vector3.SmoothDamp(agent.transform.up, centerOfGravity, ref currentVelocity, AgentSmoothTime );
return centerOfGravity;
}
}
public class AlignmentBehaviour : FlockBehaviour
{
public override Vector3 CalculateMove(Vector3 pos, FlockAgent agent, Flock flock)
{
// no neighbours, maintain heading
if (agent.Context.Count == 0)
return agent.transform.up;
// get the average direction of all neighbours in range
var avgHeading = Vector3.zero;
agent.Context.ForEach(p => avgHeading += p.up);
avgHeading /= agent.Context.Count;
return avgHeading;
}
}
and of course a way to control all this mess:
public class Flock : MonoBehaviour
{
public FlockAgent AgentPrefab;
List<FlockAgent> agents = new List<FlockAgent>();
public FlockBehaviour Behaviour;
[Range(1, 500)]
public int StartingCount = 250;
const float agentDensity = 0.08f;
[Range(1, 100)]
public float DriveFactor = 10;
[Range(1, 100)]
public float MaxSpeed = 5;
public Transform Target;
[Range(1, 10)]
public float NeighbourRadius = 1.5f;
[Range(0, 1)]
public float AvoidanceRadiusMultiplier = 0.5f;
[Range(1, 50)]
public float ObstacleRadius = 5f;
[Range(0, 1)]
public float ObstacleAvoidanceRadiusMultiplier = 0.5f;
float squareMaxSpeed;
float squareNeighbourRadius;
float squareAgentAvoidanceRadius;
float squareObstacleRadius;
float squareObstacleAvoidanceRadius;
public float SquareAgentAvoidanceRadius { get { return squareAgentAvoidanceRadius; } }
public float SquareObstcleAvoidanceRadius { get { return squareObstacleAvoidanceRadius; } }
public int seed;
private void Start()
{
UnityEngine.Random.InitState(seed);
//Behaviour = Instantiate(Behaviour);
squareMaxSpeed = MaxSpeed * MaxSpeed;
squareNeighbourRadius = NeighbourRadius * NeighbourRadius;
squareAgentAvoidanceRadius = squareNeighbourRadius * AvoidanceRadiusMultiplier * AvoidanceRadiusMultiplier;
squareObstacleRadius = ObstacleRadius * ObstacleRadius;
squareObstacleAvoidanceRadius = squareObstacleRadius * ObstacleAvoidanceRadiusMultiplier * ObstacleAvoidanceRadiusMultiplier;
var temp = Mathf.Max(1, (TemporalPartitions - 1f));
if (StartingCount / (int)temp != StartingCount / temp)
{
var remainder = StartingCount % (TemporalPartitions - 1);
StartingCount -= remainder;
}
//var numGroups = 3;
//var agentsPerGroup = StartingCount / numGroups;
//for (int i = 0; i < numGroups; i++)
//{
// var groupContext = new List<Transform>();
// var groupAgents = new List<FlockAgent>();
// var groupStart = (Vector2)transform.position + UnityEngine.Random.insideUnitCircle * StartingCount * agentDensity;
// var groupColor = Color.Lerp(Color.white, Color.black, UnityEngine.Random.Range(0f, 1f));
// for (int n = 0; n < agentsPerGroup; n++)
// {
// var newAgent = Instantiate(
// AgentPrefab,
// groupStart,
// Quaternion.Euler(0, 0, UnityEngine.Random.Range(0, 360)),
// transform
// );
// newAgent.name = "Group"+i+ "Agent" + n;
// newAgent.Initialize(this);
// newAgent.tag = this.tag;
// newAgent.gameObject.layer = gameObject.layer;
// newAgent.GetComponentInChildren<SpriteRenderer>().color = groupColor;
// groupContext.Add(newAgent.transform);
// groupAgents.Add(newAgent);
// agents.Add(newAgent);
// }
// foreach (var agent in groupAgents)
// {
// agent.Context = groupContext;
// }
//}
for (int i = 0; i < StartingCount; i++)
{
var newAgent = Instantiate(
AgentPrefab,
(Vector2)transform.position + UnityEngine.Random.insideUnitCircle * StartingCount * agentDensity,
Quaternion.Euler(0, 0, UnityEngine.Random.Range(0, 360)),
transform
);
newAgent.name = "Agent" + i;
newAgent.Initialize(this);
newAgent.tag = this.tag;
newAgent.gameObject.layer = gameObject.layer;
agents.Add(newAgent);
}
//agents.ForEach(p => p.GetNeighbours(NeighbourRadius, NeighborMask));
}
public LayerMask NeighborMask, ObstacleMask;
[Range(1, 5)]
public int TemporalPartitions = 2;
private int currentTemporalPartition = 0;
private void Update()
{
int agentsPerTemporalPartition = agents.Count / (TemporalPartitions);
int startIndex = (currentTemporalPartition) * agentsPerTemporalPartition;
int endIndex = startIndex + agentsPerTemporalPartition;
for (int i = startIndex; i < endIndex; i++)
{
var agent = agents[i];
agent.GetNeighbours(NeighbourRadius, NeighborMask);
var move = Behaviour.CalculateMove(agent.transform.position, agent, this);
if (agent.Context.Count > 0)
{
move *= DriveFactor;
if (move.sqrMagnitude > squareMaxSpeed)
move = move.normalized * MaxSpeed;
}
else
move = move.normalized * MaxSpeed;
agent.MoveVec = move;
}
if (currentTemporalPartition < TemporalPartitions - 1)
currentTemporalPartition++;
else
currentTemporalPartition = 0;
foreach (var agent in agents)
agent.Move(agent.MoveVec);
}
}