Initial commit.
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
*.exe
|
||||||
|
*.sum
|
||||||
|
working/
|
||||||
|
.vscode_build/
|
||||||
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}",
|
||||||
|
"output": "${workspaceFolder}/.vscode_build/debug_bin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
50
colliders/baserectcollider.go
Normal file
50
colliders/baserectcollider.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package colliders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fluids/gamedata"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseRectCollider struct {
|
||||||
|
position gamedata.Vector
|
||||||
|
dimensions gamedata.Vector
|
||||||
|
iscontainer bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseRectCollider) SetContainer(v bool) {
|
||||||
|
b.iscontainer = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseRectCollider) SetPosition(p gamedata.Vector) {
|
||||||
|
b.position = p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseRectCollider) GetPosition() gamedata.Vector {
|
||||||
|
return b.position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseRectCollider) SetDimensions(d gamedata.Vector) {
|
||||||
|
b.dimensions = d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseRectCollider) GetDimensions() gamedata.Vector {
|
||||||
|
return b.dimensions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseRectCollider) GetBounds() image.Rectangle {
|
||||||
|
r := image.Rectangle{
|
||||||
|
Min: image.Point{
|
||||||
|
X: int(b.position.X - b.dimensions.X/2),
|
||||||
|
Y: int(b.position.Y - b.dimensions.Y/2),
|
||||||
|
},
|
||||||
|
Max: image.Point{
|
||||||
|
X: int(b.position.X + b.dimensions.X/2),
|
||||||
|
Y: int(b.position.Y + b.dimensions.Y/2),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseRectCollider) IsContainer() bool {
|
||||||
|
return b.iscontainer
|
||||||
|
}
|
||||||
15
colliders/collider.go
Normal file
15
colliders/collider.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package colliders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fluids/gamedata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Collider interface {
|
||||||
|
GetPosition() gamedata.Vector
|
||||||
|
GetDimensions() gamedata.Vector
|
||||||
|
SetPosition(gamedata.Vector)
|
||||||
|
//GetBounds() image.Rectangle
|
||||||
|
|
||||||
|
//indicates whether this collider is inverted, i.e. 'contains' elements
|
||||||
|
//IsContainer() bool
|
||||||
|
}
|
||||||
8
elements/box.go
Normal file
8
elements/box.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import "fluids/gamedata"
|
||||||
|
|
||||||
|
type Box struct {
|
||||||
|
Position gamedata.Vector
|
||||||
|
Dimensions gamedata.Vector
|
||||||
|
}
|
||||||
25
elements/particle.go
Normal file
25
elements/particle.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import "fluids/gamedata"
|
||||||
|
|
||||||
|
type Particle struct {
|
||||||
|
Position gamedata.Vector
|
||||||
|
Velocity gamedata.Vector
|
||||||
|
Radius float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Particle) GetDimensions() gamedata.Vector {
|
||||||
|
dim := gamedata.Vector{
|
||||||
|
X: p.Radius * 2,
|
||||||
|
Y: p.Radius * 2,
|
||||||
|
}
|
||||||
|
return dim
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Particle) GetPosition() gamedata.Vector {
|
||||||
|
return p.Position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Particle) SetPosition(position gamedata.Vector) {
|
||||||
|
p.Position = position
|
||||||
|
}
|
||||||
351
game/game.go
Normal file
351
game/game.go
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fluids/elements"
|
||||||
|
"fluids/gamedata"
|
||||||
|
"fluids/quadtree"
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GameWidth = 640
|
||||||
|
GameHeight = 360
|
||||||
|
GameParticleCount = 1000
|
||||||
|
GameGravity = 2
|
||||||
|
GameParticleRadius = 5
|
||||||
|
GameDamping = .7
|
||||||
|
GameDeltaTimeStep = 0.5
|
||||||
|
|
||||||
|
GameInfluenceRadius = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
particles []*elements.Particle
|
||||||
|
cycle int
|
||||||
|
particlebox *gamedata.Vector
|
||||||
|
particlebuff *ebiten.Image
|
||||||
|
quadtree *quadtree.Quadtree
|
||||||
|
collisionquad quadtree.Quadrant
|
||||||
|
paused bool
|
||||||
|
renderquads bool
|
||||||
|
resolvecollisions bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
particlebuff: ebiten.NewImage(GameParticleRadius*2, GameParticleRadius*2),
|
||||||
|
paused: false,
|
||||||
|
renderquads: false,
|
||||||
|
resolvecollisions: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
g.particlebox = &gamedata.Vector{
|
||||||
|
X: GameWidth - 50,
|
||||||
|
Y: GameHeight - 50,
|
||||||
|
}
|
||||||
|
|
||||||
|
quad := quadtree.Quadrant{
|
||||||
|
Position: gamedata.Vector{X: GameWidth / 2, Y: GameHeight / 2},
|
||||||
|
Dimensions: gamedata.Vector{X: GameWidth, Y: GameHeight},
|
||||||
|
}
|
||||||
|
g.quadtree = quadtree.New(quad)
|
||||||
|
|
||||||
|
vector.FillCircle(g.particlebuff, GameParticleRadius, GameParticleRadius, GameParticleRadius, color.White, true)
|
||||||
|
|
||||||
|
g.collisionquad = quadtree.Quadrant{
|
||||||
|
Position: gamedata.Vector{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
Dimensions: gamedata.Vector{
|
||||||
|
X: GameParticleRadius * 2,
|
||||||
|
Y: GameParticleRadius * 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//g.InitializeColliders()
|
||||||
|
g.InitializeParticles()
|
||||||
|
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Update() error {
|
||||||
|
|
||||||
|
g.ParseInputs()
|
||||||
|
g.RebuildQuadtree()
|
||||||
|
|
||||||
|
if !g.paused {
|
||||||
|
g.UpdateParticles()
|
||||||
|
}
|
||||||
|
|
||||||
|
g.cycle++
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
|
screen.Clear()
|
||||||
|
g.RenderParticles(screen)
|
||||||
|
g.RenderBox(screen)
|
||||||
|
|
||||||
|
if g.renderquads {
|
||||||
|
g.RenderQuadrants(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Layout(x, y int) (int, int) {
|
||||||
|
return GameWidth, GameHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) RenderParticles(img *ebiten.Image) {
|
||||||
|
// mx, my := ebiten.CursorPosition()
|
||||||
|
//clr := color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
|
||||||
|
|
||||||
|
for _, particle := range g.particles {
|
||||||
|
|
||||||
|
x0 := particle.Position.X - GameParticleRadius
|
||||||
|
y0 := particle.Position.Y - GameParticleRadius
|
||||||
|
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(x0, y0)
|
||||||
|
img.DrawImage(g.particlebuff, op)
|
||||||
|
|
||||||
|
//vector.FillCircle(img, float32(particle.Position.X), float32(particle.Position.Y), float32(particle.Radius), color.White, true)
|
||||||
|
// vector.StrokeCircle(img, float32(particle.Position.X), float32(particle.Position.Y), GameInfluenceRadius, 1, clr, true)
|
||||||
|
// vector.StrokeLine(img, float32(mx), float32(my), float32(particle.Position.X), float32(particle.Position.Y), 2, clr, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) RenderBox(img *ebiten.Image) {
|
||||||
|
x0 := (GameWidth - g.particlebox.X) / 2
|
||||||
|
y0 := (GameHeight - g.particlebox.Y) / 2
|
||||||
|
vector.StrokeRect(img, float32(x0), float32(y0), float32(g.particlebox.X), float32(g.particlebox.Y), 2, color.White, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) InitializeColliders() {
|
||||||
|
/*
|
||||||
|
//box container
|
||||||
|
box := &colliders.BaseRectCollider{}
|
||||||
|
box.SetDimensions(gamedata.Vector{
|
||||||
|
X: GameWidth - 50,
|
||||||
|
Y: GameHeight - 50,
|
||||||
|
})
|
||||||
|
box.SetPosition(gamedata.Vector{
|
||||||
|
X: GameWidth / 2,
|
||||||
|
Y: GameHeight / 2,
|
||||||
|
})
|
||||||
|
box.SetContainer(true)
|
||||||
|
g.rectColliders = append(g.rectColliders, box)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) InitializeParticles() {
|
||||||
|
|
||||||
|
g.particles = g.particles[:0]
|
||||||
|
|
||||||
|
xmin := (GameWidth-g.particlebox.X)/2 + GameParticleRadius
|
||||||
|
xmax := g.particlebox.X - GameParticleRadius*2
|
||||||
|
ymin := (GameHeight-g.particlebox.Y)/2 + GameParticleRadius
|
||||||
|
ymax := g.particlebox.Y - GameParticleRadius*2
|
||||||
|
|
||||||
|
for i := 0; i < GameParticleCount; i++ {
|
||||||
|
|
||||||
|
p := &elements.Particle{
|
||||||
|
Position: gamedata.Vector{
|
||||||
|
X: xmin + rand.Float64()*xmax,
|
||||||
|
Y: ymin + rand.Float64()*ymax,
|
||||||
|
},
|
||||||
|
Velocity: gamedata.Vector{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
Radius: GameParticleRadius,
|
||||||
|
}
|
||||||
|
|
||||||
|
g.particles = append(g.particles, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) UpdateParticles() {
|
||||||
|
|
||||||
|
dt := GameDeltaTimeStep
|
||||||
|
|
||||||
|
/*
|
||||||
|
mx, my := ebiten.CursorPosition()
|
||||||
|
mpos := gamedata.Vector{X: float64(mx), Y: float64(my)}
|
||||||
|
maxdeflect := 40 * GameDeltaTimeStep * GameGravity
|
||||||
|
*/
|
||||||
|
|
||||||
|
for _, particle := range g.particles {
|
||||||
|
particle.Velocity.Y += GameGravity * dt
|
||||||
|
|
||||||
|
//if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
||||||
|
/*
|
||||||
|
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
||||||
|
delta := gamedata.Vector{
|
||||||
|
X: mpos.X - particle.Position.X,
|
||||||
|
Y: mpos.Y - particle.Position.Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
dist := math.Sqrt(delta.X*delta.X + delta.Y*delta.Y)
|
||||||
|
theta := math.Atan2(delta.Y, delta.X)
|
||||||
|
|
||||||
|
if dist < GameInfluenceRadius {
|
||||||
|
|
||||||
|
dx := dist * math.Cos(theta)
|
||||||
|
dy := dist * math.Sin(theta)
|
||||||
|
|
||||||
|
if dx != 0 {
|
||||||
|
gainx := (-1./GameInfluenceRadius)*math.Abs(dx) + 1.
|
||||||
|
particle.Velocity.X += 20 * gainx * -1 * math.Copysign(1, dx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dy != 0 {
|
||||||
|
gainy := (-1./GameInfluenceRadius)*math.Abs(dy) + 1.
|
||||||
|
particle.Velocity.Y += maxdeflect * gainy * -1 * math.Copysign(1, dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
particle.Position.X += particle.Velocity.X * dt
|
||||||
|
particle.Position.Y += particle.Velocity.Y * dt
|
||||||
|
|
||||||
|
if g.resolvecollisions {
|
||||||
|
g.ResolveCollisions(particle)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.BoundParticle(particle)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) BoundParticle(p *elements.Particle) {
|
||||||
|
|
||||||
|
xmin := (GameWidth-g.particlebox.X)/2 + p.Radius
|
||||||
|
xmax := xmin + g.particlebox.X - p.Radius*2
|
||||||
|
|
||||||
|
if p.Position.X > xmax {
|
||||||
|
p.Velocity.X *= -1 * GameDamping
|
||||||
|
p.Position.X = xmax
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Position.X < xmin {
|
||||||
|
p.Velocity.X *= -1 * GameDamping
|
||||||
|
p.Position.X = xmin
|
||||||
|
}
|
||||||
|
|
||||||
|
ymin := (GameHeight-g.particlebox.Y)/2 + p.Radius
|
||||||
|
ymax := ymin + g.particlebox.Y - p.Radius*2
|
||||||
|
|
||||||
|
if p.Position.Y > ymax {
|
||||||
|
p.Velocity.Y *= -1 * GameDamping
|
||||||
|
p.Position.Y = ymax
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Position.Y < ymin {
|
||||||
|
p.Velocity.Y *= -1 * GameDamping
|
||||||
|
p.Position.Y = ymin
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) RenderQuadrants(img *ebiten.Image) {
|
||||||
|
clr := color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
|
||||||
|
|
||||||
|
quadrants := g.quadtree.GetQuadrants()
|
||||||
|
for _, quad := range quadrants {
|
||||||
|
ox := float32(quad.Position.X - quad.Dimensions.X/2)
|
||||||
|
oy := float32(quad.Position.Y - quad.Dimensions.Y/2)
|
||||||
|
vector.StrokeRect(img, ox, oy, float32(quad.Dimensions.X), float32(quad.Dimensions.Y), 1, clr, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) ParseInputs() {
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
|
||||||
|
g.InitializeParticles()
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyP) {
|
||||||
|
g.paused = !g.paused
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
|
||||||
|
g.renderquads = !g.renderquads
|
||||||
|
}
|
||||||
|
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyC) {
|
||||||
|
g.resolvecollisions = !g.resolvecollisions
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) RebuildQuadtree() {
|
||||||
|
g.quadtree.Clear()
|
||||||
|
|
||||||
|
for _, p := range g.particles {
|
||||||
|
if !g.quadtree.Insert(p) {
|
||||||
|
fmt.Println("quadtree insertion failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) ResolveCollisions(particle *elements.Particle) {
|
||||||
|
//construct search quadrant from current particle
|
||||||
|
quadrant := quadtree.Quadrant{
|
||||||
|
Position: particle.Position,
|
||||||
|
Dimensions: particle.GetDimensions(),
|
||||||
|
}
|
||||||
|
|
||||||
|
//find list of possible maybe collisions, we inspect those in more detail
|
||||||
|
maybes := g.quadtree.FindAll(quadrant)
|
||||||
|
|
||||||
|
sqdist := float64(GameParticleRadius*GameParticleRadius) * 4
|
||||||
|
|
||||||
|
for _, p := range maybes {
|
||||||
|
if p == particle {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pos := p.GetPosition()
|
||||||
|
delta := gamedata.Vector{
|
||||||
|
X: pos.X - particle.Position.X,
|
||||||
|
Y: pos.Y - particle.Position.Y,
|
||||||
|
}
|
||||||
|
dist2 := delta.X*delta.X + delta.Y*delta.Y
|
||||||
|
|
||||||
|
if dist2 == 0 {
|
||||||
|
// Same position: pick a fallback direction to avoid NaN
|
||||||
|
delta.X = 1
|
||||||
|
delta.Y = 0
|
||||||
|
dist2 = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if dist2 < sqdist {
|
||||||
|
d := math.Sqrt(dist2)
|
||||||
|
nx, ny := delta.X/d, delta.Y/d
|
||||||
|
overlap := GameParticleRadius*2 - d
|
||||||
|
|
||||||
|
pos.X += nx * overlap
|
||||||
|
pos.Y += ny * overlap
|
||||||
|
p.SetPosition(pos)
|
||||||
|
/*
|
||||||
|
newpos := gamedata.Vector{
|
||||||
|
X: pos.X + delta.X,
|
||||||
|
Y: pos.Y + delta.Y,
|
||||||
|
}
|
||||||
|
p.SetPosition(newpos)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
gamedata/vector.go
Normal file
6
gamedata/vector.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package gamedata
|
||||||
|
|
||||||
|
type Vector struct {
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
}
|
||||||
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module fluids
|
||||||
|
|
||||||
|
go 1.24.1
|
||||||
|
|
||||||
|
require github.com/hajimehoshi/ebiten/v2 v2.9.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
|
||||||
|
github.com/ebitengine/hideconsole v1.0.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.9.0 // indirect
|
||||||
|
github.com/jezek/xgb v1.1.1 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
)
|
||||||
29
main.go
Normal file
29
main.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fluids/game"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScreenWidth = 640
|
||||||
|
ScreenHeight = 360
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("fluid experiments")
|
||||||
|
|
||||||
|
g := game.NewGame()
|
||||||
|
|
||||||
|
ebiten.SetWindowTitle("fluids")
|
||||||
|
ebiten.SetWindowSize(ScreenWidth*2, ScreenHeight*2)
|
||||||
|
|
||||||
|
err := ebiten.RunGame(g)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
192
quadtree/quadtree.go
Normal file
192
quadtree/quadtree.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package quadtree
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fluids/colliders"
|
||||||
|
"fluids/gamedata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuadtreeMaxColliders = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type Quadrant struct {
|
||||||
|
Position gamedata.Vector
|
||||||
|
Dimensions gamedata.Vector
|
||||||
|
}
|
||||||
|
|
||||||
|
type Quadtree struct {
|
||||||
|
quadrant Quadrant
|
||||||
|
children []*Quadtree
|
||||||
|
colliders []colliders.Collider
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(quadrant Quadrant) *Quadtree {
|
||||||
|
qt := &Quadtree{
|
||||||
|
quadrant: quadrant,
|
||||||
|
}
|
||||||
|
return qt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Quadtree) Insert(obj colliders.Collider) bool {
|
||||||
|
|
||||||
|
var result bool = false
|
||||||
|
//check that object meets containment criteria
|
||||||
|
if q.ContainsPoint(obj.GetPosition()) {
|
||||||
|
|
||||||
|
//if we have children, we go deeper
|
||||||
|
if len(q.children) > 0 {
|
||||||
|
for _, node := range q.children {
|
||||||
|
if node.Insert(obj) {
|
||||||
|
result = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if len(q.colliders) < QuadtreeMaxColliders {
|
||||||
|
//otherwise, if we have space, add
|
||||||
|
q.colliders = append(q.colliders, obj)
|
||||||
|
result = true
|
||||||
|
} else {
|
||||||
|
//otherwise we have to subdivide, reorganize, and add
|
||||||
|
result = q.SubdivideAndInsert(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Quadtree) ContainsPoint(p gamedata.Vector) bool {
|
||||||
|
var result bool = false
|
||||||
|
if (q.quadrant.Position.X-q.quadrant.Dimensions.X/2) <= p.X &&
|
||||||
|
p.X <= (q.quadrant.Position.X+q.quadrant.Dimensions.X/2) &&
|
||||||
|
(q.quadrant.Position.Y-q.quadrant.Dimensions.Y/2) <= p.Y &&
|
||||||
|
p.Y <= (q.quadrant.Position.Y+q.quadrant.Dimensions.Y/2) {
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Quadtree) Remove(obj colliders.Collider) bool {
|
||||||
|
var result bool = false
|
||||||
|
|
||||||
|
if q.ContainsPoint(obj.GetPosition()) {
|
||||||
|
|
||||||
|
if len(q.colliders) > 0 {
|
||||||
|
//examine existing colliders
|
||||||
|
var collection []colliders.Collider
|
||||||
|
for _, node := range q.colliders {
|
||||||
|
if node == obj {
|
||||||
|
result = true
|
||||||
|
} else {
|
||||||
|
collection = append(collection, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q.colliders = collection
|
||||||
|
} else {
|
||||||
|
//need to check children
|
||||||
|
for _, child := range q.children {
|
||||||
|
if child.Remove(obj) {
|
||||||
|
result = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Quadtree) SubdivideAndInsert(obj colliders.Collider) bool {
|
||||||
|
|
||||||
|
var result bool = false
|
||||||
|
|
||||||
|
//initialize up children
|
||||||
|
q.children = q.children[:0]
|
||||||
|
q.children = append(q.children, New(Quadrant{Position: gamedata.Vector{X: q.quadrant.Position.X - q.quadrant.Dimensions.X/4, Y: q.quadrant.Position.Y - q.quadrant.Dimensions.Y/4}, Dimensions: gamedata.Vector{X: q.quadrant.Dimensions.X / 2, Y: q.quadrant.Dimensions.Y / 2}}))
|
||||||
|
q.children = append(q.children, New(Quadrant{Position: gamedata.Vector{X: q.quadrant.Position.X + q.quadrant.Dimensions.X/4, Y: q.quadrant.Position.Y - q.quadrant.Dimensions.Y/4}, Dimensions: gamedata.Vector{X: q.quadrant.Dimensions.X / 2, Y: q.quadrant.Dimensions.Y / 2}}))
|
||||||
|
q.children = append(q.children, New(Quadrant{Position: gamedata.Vector{X: q.quadrant.Position.X - q.quadrant.Dimensions.X/4, Y: q.quadrant.Position.Y + q.quadrant.Dimensions.Y/4}, Dimensions: gamedata.Vector{X: q.quadrant.Dimensions.X / 2, Y: q.quadrant.Dimensions.Y / 2}}))
|
||||||
|
q.children = append(q.children, New(Quadrant{Position: gamedata.Vector{X: q.quadrant.Position.X + q.quadrant.Dimensions.X/4, Y: q.quadrant.Position.Y + q.quadrant.Dimensions.Y/4}, Dimensions: gamedata.Vector{X: q.quadrant.Dimensions.X / 2, Y: q.quadrant.Dimensions.Y / 2}}))
|
||||||
|
|
||||||
|
//move colliders into child nodes
|
||||||
|
var failed bool = false
|
||||||
|
for _, collider := range q.colliders {
|
||||||
|
var added bool = false
|
||||||
|
for _, node := range q.children {
|
||||||
|
if node.Insert(collider) {
|
||||||
|
added = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//couldn't add collider into any subdivided node; failure
|
||||||
|
if !added {
|
||||||
|
failed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//if all went well:
|
||||||
|
// a. need to wipe colliders from current node
|
||||||
|
// b. need to now insert new collider into one of the children
|
||||||
|
if !failed {
|
||||||
|
q.colliders = q.colliders[:0]
|
||||||
|
|
||||||
|
for _, node := range q.children {
|
||||||
|
if node.Insert(obj) {
|
||||||
|
result = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Quadtree) Clear() {
|
||||||
|
q.colliders = q.colliders[:0]
|
||||||
|
q.children = q.children[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Quadtree) GetQuadrants() []Quadrant {
|
||||||
|
var result []Quadrant
|
||||||
|
|
||||||
|
//if we have children, traverse and add their quadrants
|
||||||
|
if len(q.children) > 0 {
|
||||||
|
for _, child := range q.children {
|
||||||
|
result = append(result, child.GetQuadrants()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, q.quadrant)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// find and retrieve all colliders in given quadrant
|
||||||
|
func (q *Quadtree) FindAll(quadrant Quadrant) []colliders.Collider {
|
||||||
|
var result []colliders.Collider
|
||||||
|
|
||||||
|
//search coordinates
|
||||||
|
sx0 := quadrant.Position.X - quadrant.Dimensions.X/2
|
||||||
|
sx1 := quadrant.Position.X + quadrant.Dimensions.X/2
|
||||||
|
sy0 := quadrant.Position.Y - quadrant.Dimensions.Y/2
|
||||||
|
sy1 := quadrant.Position.Y + quadrant.Dimensions.Y/2
|
||||||
|
|
||||||
|
//quadtree coordinates
|
||||||
|
qx0 := q.quadrant.Position.X - q.quadrant.Dimensions.X/2
|
||||||
|
qx1 := q.quadrant.Position.X + q.quadrant.Dimensions.X/2
|
||||||
|
qy0 := q.quadrant.Position.Y - q.quadrant.Dimensions.Y/2
|
||||||
|
qy1 := q.quadrant.Position.Y + q.quadrant.Dimensions.Y/2
|
||||||
|
|
||||||
|
//AABB check
|
||||||
|
if sx0 < qx1 && sx1 > qx0 &&
|
||||||
|
sy0 < qy1 && sy1 > qy0 {
|
||||||
|
|
||||||
|
//if we have children, check each of those
|
||||||
|
for _, node := range q.children {
|
||||||
|
result = append(result, node.FindAll(quadrant)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, q.colliders...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user