Brought in screen manager, minor refactor.
This commit is contained in:
501
screens/game.go
Normal file
501
screens/game.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"mover/assets"
|
||||
"mover/elements"
|
||||
"mover/fonts"
|
||||
"mover/gamedata"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"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 {
|
||||
background *ebiten.Image
|
||||
collisionMask *ebiten.Image
|
||||
projectileMask *ebiten.Image
|
||||
heroCollisionMask *ebiten.Image
|
||||
heroCollisionCpy *ebiten.Image
|
||||
|
||||
dimensions gamedata.Area
|
||||
Pos gamedata.Coordinates
|
||||
Paused bool
|
||||
initialized bool
|
||||
gameover bool
|
||||
reset bool
|
||||
runtime float64
|
||||
hero *elements.Hero
|
||||
projectiles map[int]*elements.Projectile
|
||||
explosion *elements.Explosion
|
||||
|
||||
score int
|
||||
counter int
|
||||
timer int
|
||||
targets []*elements.Mover
|
||||
|
||||
gamepadIDsBuf []ebiten.GamepadID
|
||||
gamepadIDs map[ebiten.GamepadID]struct{}
|
||||
//axes map[ebiten.GamepadID][]string
|
||||
//pressedButtons map[ebiten.GamepadID][]string
|
||||
}
|
||||
|
||||
func (g *Game) Initialize() {
|
||||
|
||||
origin := gamedata.Coordinates{X: 640 / 2, Y: 480 / 2}
|
||||
|
||||
g.ConstructBackground()
|
||||
g.hero = elements.NewHero()
|
||||
g.hero.SetOrigin(origin)
|
||||
g.hero.ToggleRotate()
|
||||
|
||||
g.gameover = false
|
||||
|
||||
g.collisionMask = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height)
|
||||
g.projectileMask = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height)
|
||||
g.heroCollisionMask = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT)
|
||||
g.heroCollisionCpy = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT)
|
||||
|
||||
g.explosion = elements.NewExplosion()
|
||||
g.explosion.SetOrigin(origin)
|
||||
g.score = 0
|
||||
g.reset = false
|
||||
|
||||
//clean up all targets
|
||||
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.
|
||||
|
||||
g.projectiles = make(map[int]*elements.Projectile)
|
||||
g.initialized = true
|
||||
g.reset = false
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
} else {
|
||||
g.StepGame()
|
||||
}
|
||||
|
||||
g.timer++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Game) Draw(screen *ebiten.Image) {
|
||||
|
||||
screen.Clear()
|
||||
screen.DrawImage(g.background, nil)
|
||||
|
||||
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)
|
||||
|
||||
op.GeoM.Reset()
|
||||
op.GeoM.Translate(0, -16)
|
||||
op.GeoM.Rotate(g.hero.Angle)
|
||||
op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y)
|
||||
screen.DrawImage(assets.ImageBank[assets.Weapon], op)
|
||||
|
||||
//secondary/upgraded weapon sprite; in testing proves sort of distracting
|
||||
/*
|
||||
if g.hero.Upgrade {
|
||||
op.GeoM.Reset()
|
||||
op.GeoM.Translate(-16, -16)
|
||||
op.GeoM.Scale(0.75, 0.75)
|
||||
op.GeoM.Translate(16, 0)
|
||||
op.GeoM.Rotate(g.hero.Angle + math.Pi)
|
||||
op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y)
|
||||
screen.DrawImage(weaponImage, 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 g.dimensions.Width, g.dimensions.Height
|
||||
}
|
||||
|
||||
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 < elements.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.hero.Update()
|
||||
g.explosion.Update()
|
||||
|
||||
g.UpdateTargets()
|
||||
g.UpdateProjectiles()
|
||||
|
||||
if !g.gameover {
|
||||
g.UpdateHeroPosition()
|
||||
//append new projectiles
|
||||
g.AppendProjectiles()
|
||||
//add new target with increasing frequency
|
||||
g.SpawnEnemies()
|
||||
//handle pulsewave updates
|
||||
g.HandlePulseWaveUpdate()
|
||||
}
|
||||
|
||||
g.CleanupTargets()
|
||||
g.counter++
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) SpawnEnemies() {
|
||||
f := 40000 / (g.counter + 1)
|
||||
if g.counter%f == 0 {
|
||||
g.targets = append(g.targets, elements.NewMover())
|
||||
|
||||
x0 := rand.Float64() * 640
|
||||
y0 := rand.Float64() * 480
|
||||
quadrant := rand.IntN(3)
|
||||
|
||||
switch quadrant {
|
||||
case 0:
|
||||
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: -MOVER_HEIGHT})
|
||||
case 1:
|
||||
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: float64(g.dimensions.Height) + MOVER_HEIGHT})
|
||||
case 2:
|
||||
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: -MOVER_WIDTH, Y: y0})
|
||||
case 3:
|
||||
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: float64(g.dimensions.Width) + x0, Y: y0})
|
||||
default:
|
||||
g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: y0})
|
||||
fmt.Println("WTF " + string(quadrant))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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 <= elements.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 == elements.MoverActionDamaged {
|
||||
//fmt.Println("potential collision")
|
||||
|
||||
//the following computes total collisions in the image using a projectile mask that is a duplicate of what is on screen
|
||||
//there's definitely room for optimization here
|
||||
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, g.dimensions.Width*g.dimensions.Height*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(gamedata.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 < elements.HeroActionDying {
|
||||
dx := g.hero.Pos.X - target.Pos.X
|
||||
dy := g.hero.Pos.Y - target.Pos.Y
|
||||
angle := math.Atan2(dy, dx)
|
||||
|
||||
//maxspeed := (float64(g.counter) + 1.) / 1000.
|
||||
maxspeed := 2.9
|
||||
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 < elements.MoverActionDying && g.hero.Action < elements.HeroActionDying {
|
||||
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.SetHit()
|
||||
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.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonFrontBottomRight) {
|
||||
|
||||
g.projectiles[g.counter] = elements.NewProjectile(gamedata.Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle, 5.)
|
||||
|
||||
if g.hero.Upgrade {
|
||||
g.projectiles[g.counter+1] = elements.NewProjectile(gamedata.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 inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonRightStick) {
|
||||
if !g.explosion.Active && !g.gameover {
|
||||
g.explosion.SetOrigin(g.hero.Pos)
|
||||
g.explosion.Reset()
|
||||
g.explosion.ToggleActivate()
|
||||
}
|
||||
}
|
||||
|
||||
if inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight) {
|
||||
if g.gameover {
|
||||
g.reset = true
|
||||
} else {
|
||||
g.Paused = !g.Paused
|
||||
}
|
||||
}
|
||||
|
||||
//account for controller sensitivity
|
||||
if !g.gameover {
|
||||
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.Left = inpx < 0
|
||||
g.hero.Pos.X += inpx * 5
|
||||
}
|
||||
|
||||
if inpy >= 0.15 || inpy <= -0.15 {
|
||||
g.hero.Pos.Y += inpy * 5
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) ConstructBackground() {
|
||||
g.background = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height)
|
||||
BLOCK_SIZE := 32
|
||||
|
||||
for i := 0; i < 640/16; i++ {
|
||||
for j := 0; j < 480/16; j++ {
|
||||
|
||||
//select random tile in x and y from tileset
|
||||
idx_y := rand.IntN(256 / BLOCK_SIZE)
|
||||
idx_x := rand.IntN(256 / BLOCK_SIZE)
|
||||
|
||||
x0 := BLOCK_SIZE * idx_x
|
||||
y0 := BLOCK_SIZE * idx_y
|
||||
x1 := x0 + BLOCK_SIZE
|
||||
y1 := y0 + BLOCK_SIZE
|
||||
|
||||
//translate for grid element we're painting
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(float64(i)*16, float64(j)*16)
|
||||
g.background.DrawImage(assets.ImageBank[assets.TileSet].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
|
||||
}
|
||||
}
|
||||
|
||||
ax := float64(rand.IntN(640/BLOCK_SIZE) * BLOCK_SIZE)
|
||||
ay := float64(rand.IntN(480/BLOCK_SIZE) * BLOCK_SIZE)
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(ax, ay)
|
||||
g.background.DrawImage(assets.ImageBank[assets.Altar], op)
|
||||
}
|
||||
|
||||
func (g *Game) SetDimensions(a gamedata.Area) {
|
||||
g.dimensions = a
|
||||
}
|
||||
|
||||
func (g *Game) SetEventHandler(e ScreenManagerEvent, f func()) {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user