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 = 2000 GameGravity = 2 GameParticleRadius = 2.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 resolvers []func(particle *elements.Particle) resolveridx int alertbox *elements.Alert } func NewGame() *Game { g := &Game{ particlebuff: ebiten.NewImage(GameParticleRadius*2, GameParticleRadius*2), paused: false, renderquads: false, resolvecollisions: false, resolveridx: 0, alertbox: elements.NewAlert(), } 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, 0) 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.resolvers = append(g.resolvers, g.ResolveCollisionsA) g.resolvers = append(g.resolvers, g.ResolveCollisionsB) //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) } if g.paused { op := &ebiten.DrawImageOptions{} op.GeoM.Translate(50, 50) screen.DrawImage(g.alertbox.Sprite, op) } } 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 - particle.Radius y0 := particle.Position.Y - particle.Radius //redness := float32(particle.Position.Y / g.particlebox.Y) //blueness := 1 - redness op := &ebiten.DrawImageOptions{} op.GeoM.Translate(x0, y0) //op.ColorScale.Scale(redness, 0, blueness, 1) 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.resolvers[g.resolveridx](particle) } } for _, p := range g.particles { g.BoundParticle(p) } } 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() { //refresh particles if inpututil.IsKeyJustPressed(ebiten.KeyR) { g.InitializeParticles() } //pause simulation if inpututil.IsKeyJustPressed(ebiten.KeyP) { g.paused = !g.paused g.alertbox.SetText("PAUSED") g.alertbox.Draw() } //show quadtree quadrants if inpututil.IsKeyJustPressed(ebiten.KeyQ) { g.renderquads = !g.renderquads } //enable collision resolution if inpututil.IsKeyJustPressed(ebiten.KeyC) { g.resolvecollisions = !g.resolvecollisions } //switch between collision resolvers if inpututil.IsKeyJustPressed(ebiten.KeyLeft) { g.resolveridx = g.resolveridx - 1 if g.resolveridx < 0 { g.resolveridx = len(g.resolvers) - 1 } } if inpututil.IsKeyJustPressed(ebiten.KeyRight) { g.resolveridx = (g.resolveridx + 1) % len(g.resolvers) } } 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) ResolveCollisionsA(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(particle.Radius*particle.Radius) * 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 := particle.Radius*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) */ } } } func (g *Game) ResolveCollisionsB(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(particle.Radius*particle.Radius) * 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 < sqdist { d := math.Sqrt(dist2) overlap := particle.Radius*2 - d theta := math.Atan2(delta.Y, delta.X) pos.X += overlap * math.Cos(theta) pos.Y += overlap * math.Sin(theta) p.SetPosition(pos) m := p.(*elements.Particle) m.Velocity.X *= -1 * GameDamping m.Velocity.Y *= -1 * GameDamping } } }