Big push to implement 10mp fluid simulations.

This commit is contained in:
2025-12-03 10:21:36 -05:00
parent ba2c798b2e
commit 719b386822
11 changed files with 1536 additions and 337 deletions

View File

@@ -3,93 +3,72 @@ 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/ebitenutil"
"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
GameWidth = 640
GameHeight = 360
GameFSDW = 200
GameFSDH = 100
)
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
//control data
cycle int
paused bool
fluidsimidx int
//key elements
fluidsimd *elements.FluidSimD
fluidsim10 []*elements.FluidSim10
alertbox *elements.Alert
//cache elements
fluidsim10width float64
fluidsim10height float64
//other
fluidsim10angle float64
mdown bool
mdx, mdy int
}
func NewGame() *Game {
g := &Game{
particlebuff: ebiten.NewImage(GameParticleRadius*2, GameParticleRadius*2),
paused: false,
renderquads: false,
resolvecollisions: false,
resolveridx: 0,
alertbox: elements.NewAlert(),
paused: false,
fluidsimidx: 0,
alertbox: elements.NewAlert(),
fluidsimd: elements.NewFluidSimD(),
//fluidsim10: elements.NewFluidSim10(gamedata.Vector{X: GameFSDW, Y: GameFSDH}),
//fluidsimgpt: elements.NewFlipFluidEntity(640, 480, 2, 1, 100),
//fluidsim10: elements.NewFluidSim10(),
}
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()
g.Initialize()
return g
}
func (g *Game) Update() error {
g.ParseInputs()
g.RebuildQuadtree()
if !g.paused {
g.UpdateParticles()
switch g.fluidsimidx {
case 0:
g.fluidsimd.Update()
case 1:
//g.fluidsimgpt.Update()
g.UpdateFluidsim10()
default:
break
}
}
g.cycle++
@@ -97,13 +76,22 @@ func (g *Game) Update() error {
return nil
}
func (g *Game) UpdateFluidsim10() {
for _, sim := range g.fluidsim10 {
sim.Update()
}
}
func (g *Game) Draw(screen *ebiten.Image) {
screen.Clear()
g.RenderParticles(screen)
g.RenderBox(screen)
if g.renderquads {
g.RenderQuadrants(screen)
switch g.fluidsimidx {
case 0:
g.RenderFluidSimD(screen)
case 1:
g.RenderFluidSim10(screen)
default:
break
}
if g.paused {
@@ -113,314 +101,161 @@ func (g *Game) Draw(screen *ebiten.Image) {
}
}
func (g *Game) RenderFluidSimD(img *ebiten.Image) {
g.fluidsimd.Draw()
pos := g.fluidsimd.GetPosition()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(pos.X, pos.Y)
img.DrawImage(g.fluidsimd.GetSprite(), op)
}
func (g *Game) RenderFluidSim10(img *ebiten.Image) {
for _, sim := range g.fluidsim10 {
sim.Draw()
pos := sim.GetPosition()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(-g.fluidsim10width/2, -g.fluidsim10height/2)
op.GeoM.Scale(1, -1)
op.GeoM.Rotate(g.fluidsim10angle)
// op.GeoM.Translate(g.fluidsim10width/2, g.fluidsim10height/2)
op.GeoM.Translate(pos.X, pos.Y)
img.DrawImage(sim.GetSprite(), op)
}
//debug info
x, y := ebiten.CursorPosition()
deg := g.fluidsim10angle * 180 / math.Pi
str := fmt.Sprintf("Mouse (x: %d, y: %d) Origin (x: %d, y: %d) Angle (%f)", x, y, GameWidth/2, GameHeight/2, deg)
ebitenutil.DebugPrint(img, str)
}
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()
//simulation specific updates
switch g.fluidsimidx {
case 0:
g.ManageFluidSimDInputs()
case 1:
//g.ManageFluidSimGPTInputs()
g.ManageFluidSim10Inputs()
default:
break
}
//pause simulation
//common updates
if inpututil.IsKeyJustPressed(ebiten.KeyP) {
g.paused = !g.paused
g.alertbox.SetText("PAUSED")
g.alertbox.Draw()
}
//swap fluid simulations
if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
g.fluidsimidx = (g.fluidsimidx + 1) % 2
}
if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
g.fluidsimidx = g.fluidsimidx - 1
if g.fluidsimidx < 0 {
g.fluidsimidx = 1
}
}
}
func (g *Game) ManageFluidSimDInputs() {
//refresh particles
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
g.fluidsimd.InitializeParticles()
}
//pause simulation
if inpututil.IsKeyJustPressed(ebiten.KeyP) {
g.fluidsimd.SetPaused(!g.fluidsimd.Paused())
}
//show quadtree quadrants
if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
g.renderquads = !g.renderquads
g.fluidsimd.SetRenderQuads(!g.fluidsimd.RenderQuads())
}
//enable collision resolution
if inpututil.IsKeyJustPressed(ebiten.KeyC) {
g.resolvecollisions = !g.resolvecollisions
g.fluidsimd.SetResolveCollisions(!g.fluidsimd.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
}
g.fluidsimd.PreviousSolver()
}
if inpututil.IsKeyJustPressed(ebiten.KeyRight) {
g.resolveridx = (g.resolveridx + 1) % len(g.resolvers)
g.fluidsimd.NextSolver()
}
}
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) ManageFluidSim10Inputs() {
//refresh particles
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
for _, sim := range g.fluidsim10 {
sim.Initialize()
g.fluidsim10angle = 0
}
}
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
g.mdown = true
g.mdx, g.mdy = ebiten.CursorPosition()
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
if g.mdown {
mx, my := ebiten.CursorPosition()
dx := float64(mx) - GameWidth/2 //g.fluidsim10.GetPosition().X
dy := float64(my) - GameHeight/2 //g.fluidsim10.GetPosition().Y
angle := math.Atan2(dy, dx)
g.fluidsim10angle = angle
for _, sim := range g.fluidsim10 {
sim.SetAngle(angle)
}
}
}
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
g.mdown = false
}
}
func (g *Game) ResolveCollisionsA(particle *elements.Particle) {
//construct search quadrant from current particle
quadrant := quadtree.Quadrant{
Position: particle.Position,
Dimensions: particle.GetDimensions(),
}
func (g *Game) ManageFluidSimGPTInputs() {
//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(),
func (g *Game) Initialize() {
g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10())
g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10())
g.fluidsim10width = float64(g.fluidsim10[0].GetSprite().Bounds().Dx())
g.fluidsim10height = float64(g.fluidsim10[0].GetSprite().Bounds().Dy())
x0 := float64(GameWidth / (len(g.fluidsim10) + 1))
for i, sim := range g.fluidsim10 {
pos := gamedata.Vector{
X: x0 * float64(i+1),
Y: GameHeight / 2.,
}
sim.SetPosition(pos)
}
//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
}
}
}