I'm currently trying to implement a basic boids simulation with godot in c#.
Currently I managed to implement evasive behavior which works fine for a boid count of roughly 300. Everything beyond that is CPU limited and the simulation starts to lag.
I want to increase the amount of boids more into a number of thousands. What I tried for now is an offload of the evasive behavior into a thread per boid. But I soon realized that one thread for each boid is probably not the right way to achieve this.
So my question is: What is the proper way to handle such amounts of nodes?
I know my code is in c#. I don't need a proper solution in c# but rather a hint on where and how to offload the workload into a thread or something like this.
My world script does the following:
- Add n boids to the scene with randomized position and rotation (It's a wip so please ignore the messy rng stuff :D )
- Start a time for text update
- Pick one boid as the chosen one for visualization and speed read
using Godot;
using System;
public class World : Node2D
{
[Export]
private PackedScene boidScene = ResourceLoader.Load("Boid.tscn") as PackedScene;
private Boid chosenOne;
private int boidCount = 350;
private string groupName = "boids";
public override void _Ready() {
base._Ready();
AddBoids();
StartTextUpdateTimer(CreateTimer());
PickChosenOne();
}
private void AddBoids() {
var random = new Random();
var screensize = GetViewport().Size;
var rng = new RandomNumberGenerator();
for (int i = 0; i < boidCount; i++) {
var boid = boidScene.Instance() as Boid;
boid.GlobalPosition = new Vector2(rng.RandfRange(0f, screensize.x), rng.RandfRange(0, screensize.y));
var direction = (random.NextDouble()* (Math.PI * 2))-Math.PI;
boid.GlobalRotation = (float)direction;
AddChild(boid);
}
}
private void StartTextUpdateTimer(Timer timer) {
timer.Connect("timeout", this, "_On_Timer_Timeout");
timer.WaitTime = 1.0f;
timer.OneShot = false;
timer.Start();
}
private Timer CreateTimer() {
var timer = new Timer();
AddChild(timer);
return timer;
}
private void _On_Timer_Timeout() {
RichTextLabel richTextLabel = GetNode("Speed") as RichTextLabel;
richTextLabel.Text = "Chosen speed: " + chosenOne.LinearVelocity.Length();
}
private void PickChosenOne() {
var index = new RandomNumberGenerator().RandiRange(0, GetTree().GetNodesInGroup(groupName).Count);
chosenOne = GetTree().GetNodesInGroup(groupName)[index] as Boid;
chosenOne.Chosen = true;
}
}
And my boid scene does the following:
- Adds itself to the boids group
- Add raycasts for collision scanning (I want to simulate the view with a viewing distance)
- On each tick: Get boids in perception and evade from closest one.
using Godot;
using System;
using System.Linq;
using System.Collections.Generic;
public class Boid : RigidBody2D
{
[Export]
private string groupName = "boids";
[Export]
public int MinSpeed { get; set;} = 80;
[Export]
public int MaxSpeed { get; set; } = 120;
[Export]
public int Torque { get; set; } = 25;
[Export]
public bool Chosen { get; set; } = false;
private int perceptionRadius = 150;
public int EvasionDistance { get; } = 80;
private float radStep = 10 * Mathf.Pi / 180;
private Color vis_color = new Color(.867f, .91f, .247f, 0.1f);
public List<RayCast2D> RayCasts { get; } = new List<RayCast2D>();
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
AddToGroup(groupName);
AddRayCasts();
ApplyInitialImpluse();
}
private void AddRayCasts()
{
float i = 0;
while (i < 2 * Mathf.Pi)
{
if (IsInVisibleArea(i))
{
var rayCast = new RayCast2D();
AddChild(rayCast);
rayCast.Enabled = true;
rayCast.CastTo = new Vector2(0, -perceptionRadius).Rotated(i);
RayCasts.Add(rayCast);
}
i += radStep;
}
}
private bool IsInVisibleArea(float i)
{
// Note: Blindspot between 225° and 315°
return (i < 5 * Mathf.Pi / 4 || i > 7 * Mathf.Pi / 4);
}
private void ApplyInitialImpluse()
{
var rng = new RandomNumberGenerator();
var impulse = new Vector2(rng.RandiRange(MinSpeed, MaxSpeed), 0).Rotated(Rotation);
ApplyImpulse(new Vector2(), impulse);
}
public override void _IntegrateForces(Physics2DDirectBodyState state)
{
base._IntegrateForces(state);
TeleportOnScreenExit(state);
MaintainSpeed();
HashSet<Vector2> nodesInPerception = GetNodesInPerception();
List<Vector2> closest = GetClosestPoints(nodesInPerception, 1);
Evade(closest, state);
}
private void Evade(List<Vector2> closest, Physics2DDirectBodyState state)
{
closest.ForEach(node =>
{
var distanceToNode = Position.DistanceTo(node);
if (distanceToNode < EvasionDistance)
{
var angle = GetAngleTo(node);
state.AngularVelocity = (-angle) * (1 / (distanceToNode / 2)) * Torque;
}
});
state.LinearVelocity = new Vector2(MinSpeed, 0).Rotated(Rotation);
}
private List<Vector2> GetClosestPoints(HashSet<Vector2> nodesInPerception, int amount)
{
List<Vector2> closest = new List<Vector2>();
if (nodesInPerception.Count > 0)
closest = nodesInPerception.OrderBy(node => Position.DistanceTo(node)).Take(amount).ToList();
return closest;
}
private HashSet<Vector2> GetNodesInPerception()
{
var setOfColliders = new HashSet<Vector2>();
RayCasts.ForEach(rayCast =>
{
if (rayCast.IsColliding())
setOfColliders.Add(rayCast.GetCollisionPoint());
});
return setOfColliders;
}
public override void _Draw()
{
base._Draw();
if (Chosen)
{
DrawCircle(new Vector2(), perceptionRadius, vis_color);
float i = 0;
while (i < 2 * Mathf.Pi)
{
if (IsInVisibleArea(i))
{
DrawLine(new Vector2(0, 0), new Vector2(0, -perceptionRadius).Rotated(i), new Color("#ff8888"), 1);
}
i += radStep;
}
}
}
private void TeleportOnScreenExit(Physics2DDirectBodyState state)
{
var xform = state.Transform;
var screensize = GetViewportRect().Size;
if (xform.origin.x < 0)
xform.origin.x = screensize.x;
if (xform.origin.x > screensize.x)
xform.origin.x = 0;
if (xform.origin.y < 0)
xform.origin.y = screensize.y;
if (xform.origin.y > screensize.y)
xform.origin.y = 0;
state.Transform = xform;
}
private void MaintainSpeed()
{
if (LinearVelocity.Length() < MinSpeed)
AppliedForce = new Vector2(MinSpeed, 0).Rotated(Rotation);
else
AppliedForce = new Vector2();
}
}
```