Initial commit.
This commit is contained in:
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)
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user