package main import ( "fmt" "image/color" "log" "math" "math/rand/v2" "mover/fonts" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text" "github.com/hajimehoshi/ebiten/v2/vector" ) const ( MOVER_WIDTH = 48 MOVER_HEIGHT = 48 ) type Game struct { collisionMask *ebiten.Image projectileMask *ebiten.Image heroCollisionMask *ebiten.Image heroCollisionCpy *ebiten.Image Pos Coordinates Paused bool initialized bool gameover bool reset bool runtime float64 hero *Hero projectiles map[int]*Projectile explosion *Explosion score int counter int timer 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.hero = NewHero() g.hero.SetOrigin(origin) g.hero.ToggleRotate() g.gameover = false g.collisionMask = ebiten.NewImage(screenWidth, screenHeight) g.projectileMask = ebiten.NewImage(screenWidth, screenHeight) g.heroCollisionMask = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT) g.heroCollisionCpy = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT) g.explosion = NewExplosion() g.explosion.SetOrigin(origin) g.score = 0 g.reset = false for j := 0; j < len(g.targets); j++ { g.targets[j] = nil } g.targets = g.targets[:0] g.score = 0 g.counter = 0 g.timer = 0 g.runtime = 0. } 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) } } if !g.initialized || g.reset { g.Initialize() g.projectiles = make(map[int]*Projectile) g.initialized = true g.reset = false } else { g.StepGame() } g.timer++ return nil } func (g *Game) Draw(screen *ebiten.Image) { g.hero.Draw() op := &ebiten.DrawImageOptions{} if !g.gameover { g.runtime = float64(g.counter) / 60. } s := fmt.Sprintf("%02.3f", g.runtime) if !g.gameover { text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) } else { if (g.counter/30)%2 == 0 { text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) } } op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2) op.GeoM.Rotate(g.hero.Angle) op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y) screen.DrawImage(g.hero.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) g.score += len(g.targets) - i } for j := i; j < len(g.targets); j++ { g.targets[j] = nil } g.targets = g.targets[:i] } func (g *Game) StepGame() { g.HandleInput() if !g.Paused { g.UpdateHeroPosition() g.hero.Update() g.explosion.Update() g.UpdateTargets() g.UpdateProjectiles() if !g.gameover { //append new projectiles g.AppendProjectiles() //add new target with increasing frequency g.AddNewTargets() //handle pulsewave updates g.HandlePulseWaveUpdate() } g.CleanupTargets() g.counter++ } } func (g *Game) AddNewTargets() { 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}) } } func (g *Game) HandlePulseWaveUpdate() { if g.explosion.Active { if g.explosion.Radius > math.Sqrt(640*640+480*480) { g.explosion.ToggleActivate() g.explosion.Reset() g.ResetTargetTouches() } //check collisions for _, target := range g.targets { dx := target.Pos.X - g.hero.Pos.X dy := target.Pos.Y - g.hero.Pos.Y r := math.Sqrt(dx*dx + dy*dy) if r >= g.explosion.Radius-5 && r <= g.explosion.Radius+5 && target.Action <= MoverActionDamaged && !target.Touched { target.ToggleColor() target.Touched = true //target.SetHit() } } } } func (g *Game) UpdateProjectiles() { 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 projectile 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 } } } } } } func (g *Game) UpdateTargets() { for _, target := range g.targets { if !target.Hit && g.hero.Action < MoverActionDying { dx := g.hero.Pos.X - target.Pos.X dy := g.hero.Pos.Y - target.Pos.Y angle := math.Atan2(dy, dx) maxspeed := 3. target.Pos.X += maxspeed * math.Cos(angle) target.Pos.Y += maxspeed * math.Sin(angle) } //compute collision with hero if g.hero.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && g.hero.Pos.X <= target.Pos.X+MOVER_WIDTH/2 && g.hero.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && g.hero.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 && target.Action < MoverActionDying { g.heroCollisionMask.Clear() g.heroCollisionMask.DrawImage(g.hero.Sprite, nil) op := &ebiten.DrawImageOptions{} op.GeoM.Reset() op.Blend = ebiten.BlendSourceIn op.GeoM.Translate((g.hero.Pos.X-target.Pos.X)-MOVER_WIDTH/2, (g.hero.Pos.Y-target.Pos.Y)-MOVER_HEIGHT/2) g.heroCollisionMask.DrawImage(target.Sprite, op) //var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4) var pixels []byte = make([]byte, MOVER_HEIGHT*MOVER_HEIGHT*4) g.heroCollisionMask.ReadPixels(pixels) for i := 0; i < len(pixels); i = i + 4 { if pixels[i+3] != 0 { fmt.Println("pixel death") g.hero.Action = MoverActionDying g.gameover = true break } } } target.Update() } } func (g *Game) ResetTargetTouches() { for _, t := range g.targets { t.Touched = false } } func (g *Game) AppendProjectiles() { if g.counter%14 == 0 && ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton7) { g.projectiles[g.counter] = NewProjectile(Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle, 5.) g.projectiles[g.counter+1] = NewProjectile(Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle+math.Pi, 5.) } } func (g *Game) HandleInput() { if len(g.gamepadIDs) > 0 { if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton11) { if !g.explosion.Active { g.explosion.SetOrigin(g.hero.Pos) g.explosion.Reset() g.explosion.ToggleActivate() } } if inpututil.IsGamepadButtonJustPressed(0, ebiten.GamepadButton9) { if g.gameover { g.reset = true } else { g.Paused = !g.Paused } } //account for controller sensitivity xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal) yaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical) if yaxis <= 0.09 && yaxis >= -0.09 { yaxis = 0 } if xaxis <= 0.09 && xaxis >= -0.09 { xaxis = 0 } inputangle := math.Atan2(yaxis, xaxis) g.hero.SetAngle(inputangle) } } func (g *Game) UpdateHeroPosition() { //handle gamepad input inpx := ebiten.GamepadAxisValue(0, 0) inpy := ebiten.GamepadAxisValue(0, 1) if inpx >= 0.15 || inpx <= -0.15 { g.hero.Pos.X += ebiten.GamepadAxisValue(0, 0) * 5 } if inpy >= 0.15 || inpy <= -0.15 { g.hero.Pos.Y += ebiten.GamepadAxisValue(0, 1) * 5 } }