package main import ( "fmt" "image/color" "log" "math" "math/rand/v2" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/vector" ) const ( MOVER_WIDTH = 48 MOVER_HEIGHT = 48 ) type Game struct { collisionMask *ebiten.Image projectileMask *ebiten.Image Pos Coordinates initialized bool mover *Mover projectiles map[int]*Projectile explosion *Explosion counter int targets []*Mover gamepadIDsBuf []ebiten.GamepadID gamepadIDs map[ebiten.GamepadID]struct{} //axes map[ebiten.GamepadID][]string //pressedButtons map[ebiten.GamepadID][]string } func (g *Game) Initialize() { origin := Coordinates{X: 640 / 2, Y: 480 / 2} g.mover = NewMover() g.mover.SetOrigin(origin) g.mover.ToggleRotate() g.collisionMask = ebiten.NewImage(screenWidth, screenHeight) g.projectileMask = ebiten.NewImage(screenWidth, screenHeight) /* single target g.target = NewMover() g.target.SetOrigin(Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) */ //g.projectiles = append(g.projectiles, NewProjectile(Coordinates{X: 640 / 2, Y: 480 / 2}, 0., 5.)) g.explosion = NewExplosion() g.explosion.SetOrigin(origin) } func (g *Game) Update() error { if g.gamepadIDs == nil { g.gamepadIDs = map[ebiten.GamepadID]struct{}{} } g.gamepadIDsBuf = inpututil.AppendJustConnectedGamepadIDs(g.gamepadIDsBuf[:0]) for _, id := range g.gamepadIDsBuf { log.Printf("gamepad connected: id: %d, SDL ID: %s", id, ebiten.GamepadSDLID(id)) g.gamepadIDs[id] = struct{}{} } for id := range g.gamepadIDs { if inpututil.IsGamepadJustDisconnected(id) { log.Printf("gamepad disconnected: id: %d", id) delete(g.gamepadIDs, id) } } //handle gamepad input inpx := ebiten.GamepadAxisValue(0, 0) inpy := ebiten.GamepadAxisValue(0, 1) if inpx >= 0.15 || inpx <= -0.15 { g.mover.Pos.X += ebiten.GamepadAxisValue(0, 0) * 5 } if inpy >= 0.15 || inpy <= -0.15 { g.mover.Pos.Y += ebiten.GamepadAxisValue(0, 1) * 5 } if !g.initialized { g.Initialize() g.projectiles = make(map[int]*Projectile) g.initialized = true } else { if len(g.gamepadIDs) > 0 { xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal) yaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical) maxButton := ebiten.GamepadButton(ebiten.GamepadButtonCount(0)) for b := ebiten.GamepadButton(0); b < maxButton; b++ { if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton7) { if !g.explosion.Active { g.explosion.SetOrigin(g.mover.Pos) g.explosion.Reset() g.explosion.ToggleActivate() } } } if yaxis <= 0.09 && yaxis >= -0.09 { yaxis = 0 } if xaxis <= 0.09 && xaxis >= -0.09 { xaxis = 0 } inputangle := math.Atan2(yaxis, xaxis) g.mover.SetAngle(inputangle) } /* for id := range g.gamepadIDs { xaxis := ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickHorizontal) yaxis := ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickVertical) inputangle := math.Atan2(yaxis, xaxis) g.mover.SetAngle(inputangle) } */ g.mover.Update() g.explosion.Update() for _, target := range g.targets { if !target.Hit { dx := g.mover.Pos.X - target.Pos.X dy := g.mover.Pos.Y - target.Pos.Y //dist := math.Sqrt(dx*dx + dy + dy) angle := math.Atan2(dy, dx) maxspeed := 3. target.Pos.X += maxspeed * math.Cos(angle) target.Pos.Y += maxspeed * math.Sin(angle) } target.Update() } for k, p := range g.projectiles { //for i := 0; i < len(g.projectiles); i++ { // g.projectiles[i].Update() p.Update() //if g.projectiles[i].Pos.X < 5 || g.projectiles[i].Pos.X > 635 || g.projectiles[i].Pos.Y < 5 || g.projectiles[i].Pos.Y > 475 { if p.Pos.X < 5 || p.Pos.X > 635 || p.Pos.Y < 5 || p.Pos.Y > 475 { p.Velocity = 0 delete(g.projectiles, k) } //compute collisions for _, target := range g.targets { //first, boundary check if p.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && p.Pos.X <= target.Pos.X+MOVER_WIDTH/2 && p.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && p.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 && target.Action == MoverActionDamaged { fmt.Println("potential collision") g.collisionMask.Clear() g.collisionMask.DrawImage(g.projectileMask, nil) op := &ebiten.DrawImageOptions{} op.GeoM.Reset() op.Blend = ebiten.BlendSourceIn op.GeoM.Translate(target.Pos.X-MOVER_WIDTH/2, target.Pos.Y-MOVER_HEIGHT/2) g.collisionMask.DrawImage(target.Sprite, op) //var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4) var pixels []byte = make([]byte, screenWidth*screenHeight*4) g.collisionMask.ReadPixels(pixels) for i := 0; i < len(pixels); i = i + 4 { if pixels[i+3] != 0 { fmt.Println("pixel collision") delete(g.projectiles, k) //target.ToggleColor() target.SetHit() //target.SetOrigin(Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) target.Hit = true break } } } } } //append new projectiles if g.counter%14 == 0 { //g.projectiles = append(g.projectiles, NewProjectile(Coordinates{X: g.mover.Pos.X, Y: g.mover.Pos.Y}, g.mover.Angle, 5.)) //g.projectiles = append(g.projectiles, NewProjectile(Coordinates{X: g.mover.Pos.X, Y: g.mover.Pos.Y}, g.mover.Angle+math.Pi, 5.)) g.projectiles[g.counter] = NewProjectile(Coordinates{X: g.mover.Pos.X, Y: g.mover.Pos.Y}, g.mover.Angle, 5.) g.projectiles[g.counter+1] = NewProjectile(Coordinates{X: g.mover.Pos.X, Y: g.mover.Pos.Y}, g.mover.Angle+math.Pi, 5.) } //add new target with increasing frequency f := 40000 / (g.counter + 1) if g.counter%f == 0 { g.targets = append(g.targets, NewMover()) g.targets[len(g.targets)-1].SetOrigin(Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) } //handle explosion updates if g.explosion.Active { if g.explosion.Radius > math.Sqrt(640*640+480*480) { g.explosion.ToggleActivate() g.explosion.Reset() } //check collisions for _, target := range g.targets { dx := target.Pos.X - g.mover.Pos.X dy := target.Pos.Y - g.mover.Pos.Y r := math.Sqrt(dx*dx + dy*dy) if r >= g.explosion.Radius-5 && r <= g.explosion.Radius+5 && target.Action <= MoverActionDefault { //target.ToggleColor() target.SetHit() } } } g.counter++ } g.CleanupTargets() return nil } func (g *Game) Draw(screen *ebiten.Image) { g.mover.Draw() op := &ebiten.DrawImageOptions{} /* dx := 40 * math.Cos(float64(g.counter)/16) dy := 40 * math.Sin(float64(g.counter)/16) a := float64(g.counter) / (math.Pi * 2) */ op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2) op.GeoM.Rotate(g.mover.Angle) op.GeoM.Translate(g.mover.Pos.X, g.mover.Pos.Y) screen.DrawImage(g.mover.Sprite, op) for _, target := range g.targets { target.Draw() op.GeoM.Reset() op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2) op.GeoM.Rotate(target.Angle) op.GeoM.Translate(target.Pos.X, target.Pos.Y) screen.DrawImage(target.Sprite, op) } g.projectileMask.Clear() //ebitenutil.DrawCircle() for _, p := range g.projectiles { //vector.DrawFilledCircle(screen, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true) vector.DrawFilledCircle(g.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true) } screen.DrawImage(g.projectileMask, nil) vector.StrokeCircle(screen, float32(g.explosion.Origin.X), float32(g.explosion.Origin.Y), float32(g.explosion.Radius), 3, color.White, true) /*for _, gamepad ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickHorizontal), ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickVertical))*/ } func (g *Game) Layout(width, height int) (int, int) { return screenWidth, screenHeight } func (g *Game) CleanupTargets() { // remove dead targets by 1) iterating over all targets i := 0 for _, target := range g.targets { //moving valid targets to the front of the slice if target.Action < MoverActionDead { g.targets[i] = target i++ } } //then culling the last elements of the slice if len(g.targets)-i > 0 { fmt.Printf("Removing %d elements\n", len(g.targets)-i) } for j := i; j < len(g.targets); j++ { g.targets[j] = nil } g.targets = g.targets[:i] }