My setup is like this. Every update my movement system moves all of the units on the field. They use some Boid logic to avoid eachother, and a flow field that directs them towards each other. This is the result. https://imgur.com/a/CaUpfT7

My general setup is to calculate 2 flow fields, one for each player. From every field on the map (32×32 fields) I calculate the vector that points towards the closest enemy. Now this is a fine approach but I’d like to somehow take into consideration obstacles somehow (without another round of for loops)? Or should I handle that in my movement system avoidance? I was maybe thinking of doing A* once a unit is close enough to the enemy but all of these systems together seem rather hacky and overkill.

There are no obstacles currently, maybe a spell will generate it in the future. For now there are only units on the battleground without terrain or obstacles (besides units fighting eachother)

Here is some code. Firstly how I create the flowfields (kinda costly, will look into a way to optimize it further, don’t really need every single tile, can probably merge 2 or 3 together)

`func (g *Grid) Update() { g.entityPositions[0] = g.entityPositions[0][:0] g.entityPositions[1] = g.entityPositions[1][:0] entities := g.world.GetEntityManager().GetEntities() posComps := g.world.GetObjectPool().Components["PositionComponent"] for _, ent := range entities { g.entityPositions[ent.PlayerTag] = append(g.entityPositions[ent.PlayerTag], posComps[ent.Index].(components.PositionComponent).Position) } for x := 0; x <= g.MaxWidth/FlowTileSize; x++ { for y := 0; y <= g.MaxHeight/FlowTileSize; y++ { curPosVector := engine.Vector{X: float32(x * 32), Y: float32(y * 32)} // find closest tile to this one for both players minDist := float32(100000.0) minIndx := -1 for indx, pos := range g.entityPositions[0] { d := engine.GetDistanceIncludingDiagonal(pos, curPosVector) if d < minDist { minIndx = indx minDist = d } } // fmt.Printf("CurPos : %v, enemyPos : %v, direction %v \n", curPosVector, g.entityPositions[0][minIndx], g.entityPositions[0][minIndx].Subtract(curPosVector).Normalize()) g.flowTiles[1][x][y].Direction = g.entityPositions[0][minIndx].Subtract(curPosVector).Normalize() minDist1 := float32(100000.0) minIndx1 := -1 for indx, pos := range g.entityPositions[1] { d := engine.GetDistanceIncludingDiagonal(pos, curPosVector) if d < minDist1 { minIndx1 = indx minDist1 = d } } g.flowTiles[0][x][y].Direction = g.entityPositions[1][minIndx1].Subtract(curPosVector).Normalize() } } } `

And my movement code. A lot of code but just basic allignnment cohesion/ separation. With added 2 look ahead vectors to steer away from the collision.

` desiredDirection := world.Grid.GetDesiredDirectionAt(positionComp.Position, tag) direction := movementComp.Direction maxSpeed = movementComp.MovementSpeed //Avoidance nearbyEntities := helper.GetNearbyEntities(100, world, index) avoidance := engine.Zero() avoidance = avoidance.Add(alignment(world, nearbyEntities, direction).MultiplyScalar(alignmentCoef)) avoidance = avoidance.Add(cohesion(world, nearbyEntities, direction, positionComp.Position).MultiplyScalar(cohesionCoef)) avoidance = avoidance.Add(separation(world, nearbyEntities, direction, positionComp.Position).MultiplyScalar(separationCoef)) //Checking ahead of us whether or not we'll encounter something lookAheadVectorLong := direction.Add(desiredDirection).MultiplyScalar(maxSpeed * 2.5) lookAheadVectorShort := direction.Add(desiredDirection).MultiplyScalar(maxSpeed) maxAvoidanceForce := float32(1.0) checkPosShort := positionComp.Position.Add(lookAheadVectorShort) checkPosLong := positionComp.Position.Add(lookAheadVectorLong) collidedIndexShort := world.Grid.IsPositionFree(index, checkPosShort, positionComp.BoundingBox) collidedIndexLong := world.Grid.IsPositionFree(index, checkPosLong, positionComp.BoundingBox) if collidedIndexShort != -1 { direction = engine.Zero() avoidance = checkPosShort.Subtract(world.ObjectPool.Components["PositionComponent"][collidedIndexShort].(components.PositionComponent).Position).Normalize() avoidance = avoidance.MultiplyScalar(maxAvoidanceForce * 1.5) } else if collidedIndexLong != -1 { direction = direction.MultiplyScalar(breakingForce) avoidance = checkPosShort.Subtract(world.ObjectPool.Components["PositionComponent"][collidedIndexLong].(components.PositionComponent).Position).Normalize() avoidance = avoidance.MultiplyScalar(maxAvoidanceForce * 1.2) } direction = desiredDirection direction = direction.Add(avoidance).Normalize() positionComp.Position = positionComp.Position.Add(direction.MultiplyScalar(maxSpeed)) positionComp.Position.X = engine.Constraint(positionComp.Position.X, 0, 799) positionComp.Position.Y = engine.Constraint(positionComp.Position.Y, 0, 511) movementComp.Direction = direction.Normalize() world.ObjectPool.Components["PositionComponent"][index] = positionComp world.ObjectPool.Components["MovementComponent"][index] = movementComp fmt.Printf("I %d am at %v\n", index, positionComp.Position) } } func limit(p engine.Vector, lim float32) engine.Vector { if p.X > lim { p.X = lim } else if p.X < -lim { p.X = -lim } if p.Y > lim { p.Y = lim } else if p.Y < -lim { p.Y = -lim } return p } func alignment(world *game.World, siblings []int, velocity engine.Vector) engine.Vector { avg := engine.Vector{X: 0, Y: 0} total := float32(0.0) for _, siblingIndex := range siblings { avg = avg.Add(world.ObjectPool.Components["MovementComponent"][siblingIndex].(components.MovementComponent).Direction) total++ } if total > 0 { avg = avg.DivideScalar(total) avg = avg.Normalize().MultiplyScalar(maxSpeed) avg = avg.Subtract(velocity) avg = limit(avg, maxForce) return avg } return engine.Vector{X: 0.0, Y: 0.0} } func cohesion(world *game.World, siblings []int, velocity engine.Vector, position engine.Vector) engine.Vector { avg := engine.Vector{X: 0, Y: 0} total := float32(0) for _, siblingindex := range siblings { avg = avg.Add(world.ObjectPool.Components["PositionComponent"][siblingIndex].(components.PositionComponent).Position) total++ } if total > 0 { avg = avg.MultiplyScalar(1.0 / total * cohesionCoef) avg = avg.Subtract(position) avg = avg.Normalize().MultiplyScalar(maxSpeed) avg = avg.Subtract(velocity) avg = limit(avg, maxForce) return avg } return engine.Vector{X: 0.0, Y: 0.0} } func separation(world *game.World, siblings []int, velocity engine.Vector, position engine.Vector) engine.Vector { avg := engine.Vector{X: 0, Y: 0} total := float32(0) for _, siblingIndex := range siblings { siblingPos := world.ObjectPool.Components["PositionComponent"][siblingIndex].(components.PositionComponent).Position d := position.Distance(siblingPos) if d < desiredSeperation { diff := position.Subtract(siblingPos) diff = diff.Normalize() diff = diff.DivideScalar(d) avg = avg.Add(diff) total++ } } if total > 0 { avg.DivideScalar(total) } if total > 0 { avg = avg.MultiplyScalar(1.0 / total * separationCoef) avg = avg.Normalize().MultiplyScalar(maxSpeed) avg = avg.Subtract(velocity) avg = limit(avg, maxForce) } return avg } `

What I am trying to achieve is:

Units not mashing into each other and just positioning themselves in a free spot around their target.

What are my problems :

- Make the flow field direct them away from collision rather than just towards closest unit.
- Make it work with the current system without adding too many nested loops and awful checks.
- I am doing the avoidance correctly? I have a desired direction that I get from the flow field (that directs me towards closest enemy), then I add avoidance to it to avoid any other units in the area.

My units move really well up untill the point of collision/ going to a spot next to a target. I am not sure how to implemenent that behaviour yet.

(This is my forth iteration of the movement system. I went from pure boid, to grid based, to A*, to this. So I tried a lot of variations and this going surrounding behaviour has been bugging me every time.)

Custom engine (server) written in Golang and then dispatched to Godot for the visuals. Performance is not my concern, but this is a server, so I am more mindful of that, I could probably brute force it but I’d rather hear some better take on it.

Any suggestion/article/ video is greatly appreciated !!

Edit: Thinking about it, flow field is currently useless. I can basically just have a vector pointing to the closest enemy for each unit… Would be less costly as well. But the problem of them clumping and not knowing how to surround still stands.