Compare commits

..

2 Commits

10 changed files with 368 additions and 42 deletions

BIN
fonts/FFForward.ttf Normal file

Binary file not shown.

BIN
fonts/agencyb.ttf Normal file

Binary file not shown.

BIN
fonts/arcade_n.ttf Normal file

Binary file not shown.

85
fonts/fonts.go Normal file
View File

@@ -0,0 +1,85 @@
package fonts
import (
"log"
_ "embed"
"github.com/hajimehoshi/ebiten/examples/resources/fonts"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/font/sfnt"
)
const (
FontDPI = 72
FontSizeStandard = 16
FontSizeLarge = 24
FontSizeBig = 60
FontSizeArcade = 12
FontSizeArcadeBig = 40
FontSizeArcadeHuge = 80
)
type FontStruct struct {
Standard font.Face
Large font.Face
Glitch font.Face
GlitchBig font.Face
Arcade font.Face
ArcadeLarge font.Face
ArcadeHuge font.Face
}
var (
//go:embed agencyb.ttf
agency_ttf []byte
//go:embed arcade_n.ttf
arcade_ttf []byte
SurviveFont FontStruct
)
func LoadFontFatal(src []byte) *sfnt.Font {
tt, err := opentype.Parse(src)
if err != nil {
log.Fatal(err)
}
return tt
}
func GetFaceFatal(fnt *sfnt.Font, dpi, size float64) font.Face {
var face font.Face
var err error
if dpi > 0 && size > 0 && fnt != nil {
face, err = opentype.NewFace(fnt, &opentype.FaceOptions{
Size: size,
DPI: dpi,
Hinting: font.HintingVertical,
})
if err != nil {
log.Fatal(err)
}
}
return face
}
func init() {
SurviveFont = FontStruct{}
fnt := LoadFontFatal(fonts.MPlus1pRegular_ttf)
SurviveFont.Standard = GetFaceFatal(fnt, FontDPI, FontSizeStandard)
SurviveFont.Large = GetFaceFatal(fnt, FontDPI, FontSizeLarge)
fnt2 := LoadFontFatal(agency_ttf)
SurviveFont.Glitch = GetFaceFatal(fnt2, FontDPI, FontSizeLarge)
SurviveFont.GlitchBig = GetFaceFatal(fnt2, FontDPI, FontSizeBig)
fnt3 := LoadFontFatal(arcade_ttf)
SurviveFont.Arcade = GetFaceFatal(fnt3, FontDPI, FontSizeArcade)
SurviveFont.ArcadeLarge = GetFaceFatal(fnt3, FontDPI, FontSizeArcadeBig)
SurviveFont.ArcadeHuge = GetFaceFatal(fnt3, FontDPI, FontSizeArcadeHuge)
}

178
game.go
View File

@@ -1,13 +1,20 @@
package main package main
import ( import (
"bytes"
"fmt"
"image"
"image/color" "image/color"
"log" "log"
"math" "math"
"math/rand/v2" "math/rand/v2"
"mover/fonts"
_ "embed"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
) )
@@ -16,17 +23,31 @@ const (
MOVER_HEIGHT = 48 MOVER_HEIGHT = 48
) )
var (
tilesetImage *ebiten.Image
//go:embed grasstile.png
tileset_img []byte
)
type Game struct { type Game struct {
background *ebiten.Image
collisionMask *ebiten.Image collisionMask *ebiten.Image
projectileMask *ebiten.Image projectileMask *ebiten.Image
heroCollisionMask *ebiten.Image
heroCollisionCpy *ebiten.Image
Pos Coordinates Pos Coordinates
Paused bool Paused bool
initialized bool initialized bool
mover *Mover gameover bool
reset bool
runtime float64
hero *Hero
projectiles map[int]*Projectile projectiles map[int]*Projectile
explosion *Explosion explosion *Explosion
score int
counter int counter int
timer int timer int
targets []*Mover targets []*Mover
@@ -37,19 +58,44 @@ type Game struct {
//pressedButtons map[ebiten.GamepadID][]string //pressedButtons map[ebiten.GamepadID][]string
} }
func init() {
img, _, err := image.Decode(bytes.NewReader(tileset_img))
if err != nil {
log.Fatal(err)
}
tilesetImage = ebiten.NewImageFromImage(img)
}
func (g *Game) Initialize() { func (g *Game) Initialize() {
origin := Coordinates{X: 640 / 2, Y: 480 / 2} origin := Coordinates{X: 640 / 2, Y: 480 / 2}
g.mover = NewMover() g.ConstructBackground()
g.mover.SetOrigin(origin) g.hero = NewHero()
g.mover.ToggleRotate() g.hero.SetOrigin(origin)
g.hero.ToggleRotate()
g.gameover = false
g.collisionMask = ebiten.NewImage(screenWidth, screenHeight) g.collisionMask = ebiten.NewImage(screenWidth, screenHeight)
g.projectileMask = 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 = NewExplosion()
g.explosion.SetOrigin(origin) 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.
} }
@@ -71,11 +117,12 @@ func (g *Game) Update() error {
} }
} }
if !g.initialized { if !g.initialized || g.reset {
g.Initialize() g.Initialize()
g.projectiles = make(map[int]*Projectile) g.projectiles = make(map[int]*Projectile)
g.initialized = true g.initialized = true
g.reset = false
} else { } else {
g.StepGame() g.StepGame()
} }
@@ -87,19 +134,32 @@ func (g *Game) Update() error {
func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Draw(screen *ebiten.Image) {
g.mover.Draw() screen.Clear()
screen.DrawImage(g.background, nil)
g.hero.Draw()
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
/* if !g.gameover {
dx := 40 * math.Cos(float64(g.counter)/16) g.runtime = float64(g.counter) / 60.
dy := 40 * math.Sin(float64(g.counter)/16) }
a := float64(g.counter) / (math.Pi * 2)
*/ 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.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2)
op.GeoM.Rotate(g.mover.Angle) op.GeoM.Rotate(g.hero.Angle)
op.GeoM.Translate(g.mover.Pos.X, g.mover.Pos.Y) op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y)
screen.DrawImage(g.mover.Sprite, op) screen.DrawImage(g.hero.Sprite, op)
for _, target := range g.targets { for _, target := range g.targets {
target.Draw() target.Draw()
@@ -142,10 +202,13 @@ func (g *Game) CleanupTargets() {
i++ i++
} }
} }
//then culling the last elements of the slice //then culling the last elements of the slice
/*if len(g.targets)-i > 0 { if len(g.targets)-i > 0 {
fmt.Printf("Removing %d elements\n", len(g.targets)-i) // fmt.Printf("Removing %d elements\n", len(g.targets)-i)
}*/ g.score += len(g.targets) - i
}
for j := i; j < len(g.targets); j++ { for j := i; j < len(g.targets); j++ {
g.targets[j] = nil g.targets[j] = nil
} }
@@ -160,20 +223,20 @@ func (g *Game) StepGame() {
g.UpdateHeroPosition() g.UpdateHeroPosition()
g.mover.Update() g.hero.Update()
g.explosion.Update() g.explosion.Update()
g.UpdateTargets() g.UpdateTargets()
g.UpdateProjectiles() g.UpdateProjectiles()
if !g.gameover {
//append new projectiles //append new projectiles
g.AppendProjectiles() g.AppendProjectiles()
//add new target with increasing frequency //add new target with increasing frequency
g.AddNewTargets() g.AddNewTargets()
//handle pulsewave updates //handle pulsewave updates
g.HandlePulseWaveUpdate() g.HandlePulseWaveUpdate()
}
g.CleanupTargets() g.CleanupTargets()
g.counter++ g.counter++
@@ -198,8 +261,8 @@ func (g *Game) HandlePulseWaveUpdate() {
//check collisions //check collisions
for _, target := range g.targets { for _, target := range g.targets {
dx := target.Pos.X - g.mover.Pos.X dx := target.Pos.X - g.hero.Pos.X
dy := target.Pos.Y - g.mover.Pos.Y dy := target.Pos.Y - g.hero.Pos.Y
r := math.Sqrt(dx*dx + dy*dy) r := math.Sqrt(dx*dx + dy*dy)
if r >= g.explosion.Radius-5 && r <= g.explosion.Radius+5 && target.Action <= MoverActionDamaged && !target.Touched { if r >= g.explosion.Radius-5 && r <= g.explosion.Radius+5 && target.Action <= MoverActionDamaged && !target.Touched {
@@ -230,6 +293,8 @@ func (g *Game) UpdateProjectiles() {
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 { 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") //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.Clear()
g.collisionMask.DrawImage(g.projectileMask, nil) g.collisionMask.DrawImage(g.projectileMask, nil)
@@ -261,9 +326,9 @@ func (g *Game) UpdateProjectiles() {
func (g *Game) UpdateTargets() { func (g *Game) UpdateTargets() {
for _, target := range g.targets { for _, target := range g.targets {
if !target.Hit { if !target.Hit && g.hero.Action < MoverActionDying {
dx := g.mover.Pos.X - target.Pos.X dx := g.hero.Pos.X - target.Pos.X
dy := g.mover.Pos.Y - target.Pos.Y dy := g.hero.Pos.Y - target.Pos.Y
angle := math.Atan2(dy, dx) angle := math.Atan2(dy, dx)
maxspeed := 3. maxspeed := 3.
@@ -271,6 +336,30 @@ func (g *Game) UpdateTargets() {
target.Pos.Y += maxspeed * math.Sin(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() target.Update()
} }
} }
@@ -284,8 +373,8 @@ func (g *Game) ResetTargetTouches() {
func (g *Game) AppendProjectiles() { func (g *Game) AppendProjectiles() {
if g.counter%14 == 0 && ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton7) { if g.counter%14 == 0 && ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton7) {
g.projectiles[g.counter] = NewProjectile(Coordinates{X: g.mover.Pos.X, Y: g.mover.Pos.Y}, g.mover.Angle, 5.) 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.mover.Pos.X, Y: g.mover.Pos.Y}, g.mover.Angle+math.Pi, 5.) g.projectiles[g.counter+1] = NewProjectile(Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle+math.Pi, 5.)
} }
} }
@@ -293,15 +382,19 @@ func (g *Game) HandleInput() {
if len(g.gamepadIDs) > 0 { if len(g.gamepadIDs) > 0 {
if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton11) { if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton11) {
if !g.explosion.Active { if !g.explosion.Active {
g.explosion.SetOrigin(g.mover.Pos) g.explosion.SetOrigin(g.hero.Pos)
g.explosion.Reset() g.explosion.Reset()
g.explosion.ToggleActivate() g.explosion.ToggleActivate()
} }
} }
if inpututil.IsGamepadButtonJustPressed(0, ebiten.GamepadButton9) { if inpututil.IsGamepadButtonJustPressed(0, ebiten.GamepadButton9) {
if g.gameover {
g.reset = true
} else {
g.Paused = !g.Paused g.Paused = !g.Paused
} }
}
//account for controller sensitivity //account for controller sensitivity
xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal) xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal)
@@ -315,7 +408,7 @@ func (g *Game) HandleInput() {
} }
inputangle := math.Atan2(yaxis, xaxis) inputangle := math.Atan2(yaxis, xaxis)
g.mover.SetAngle(inputangle) g.hero.SetAngle(inputangle)
} }
} }
@@ -324,10 +417,33 @@ func (g *Game) UpdateHeroPosition() {
inpx := ebiten.GamepadAxisValue(0, 0) inpx := ebiten.GamepadAxisValue(0, 0)
inpy := ebiten.GamepadAxisValue(0, 1) inpy := ebiten.GamepadAxisValue(0, 1)
if inpx >= 0.15 || inpx <= -0.15 { if inpx >= 0.15 || inpx <= -0.15 {
g.mover.Pos.X += ebiten.GamepadAxisValue(0, 0) * 5 g.hero.Pos.X += ebiten.GamepadAxisValue(0, 0) * 5
} }
if inpy >= 0.15 || inpy <= -0.15 { if inpy >= 0.15 || inpy <= -0.15 {
g.mover.Pos.Y += ebiten.GamepadAxisValue(0, 1) * 5 g.hero.Pos.Y += ebiten.GamepadAxisValue(0, 1) * 5
}
}
func (g *Game) ConstructBackground() {
g.background = ebiten.NewImage(screenWidth, screenHeight)
for i := 0; i < 640/16; i++ {
for j := 0; j < 480/16; j++ {
//select random tile in x and y from tileset
idx_x := rand.IntN(256 / 16)
idx_y := rand.IntN(256 / 16)
x0 := 16 * idx_x
y0 := 16 * idx_y
x1 := x0 + 16
y1 := y0 + 16
//translate for grid element we're painting
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(i)*16, float64(j)*16)
g.background.DrawImage(tilesetImage.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op)
}
} }
} }

7
go.mod
View File

@@ -4,7 +4,11 @@ go 1.22.0
toolchain go1.22.8 toolchain go1.22.8
require github.com/hajimehoshi/ebiten/v2 v2.8.2 require (
github.com/hajimehoshi/ebiten v1.12.12
github.com/hajimehoshi/ebiten/v2 v2.8.2
golang.org/x/image v0.20.0
)
require ( require (
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect
@@ -13,4 +17,5 @@ require (
github.com/jezek/xgb v1.1.1 // indirect github.com/jezek/xgb v1.1.1 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
) )

BIN
grasstile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

120
hero.go Normal file
View File

@@ -0,0 +1,120 @@
package main
import (
"bytes"
"image"
"log"
_ "embed"
"image/color"
_ "image/png"
"github.com/hajimehoshi/ebiten/v2"
)
var (
heroImage *ebiten.Image
//go:embed hero.png
hero_img []byte
)
const (
HeroActionDefault = iota
HeroActionDamaged
HeroActionDying
HeroActionExploding
HeroActionDead
HeroActionMax
)
type HeroAction uint
func init() {
img, _, err := image.Decode(bytes.NewReader(hero_img))
if err != nil {
log.Fatal(err)
}
heroImage = ebiten.NewImageFromImage(img)
}
type Hero struct {
Sprite *ebiten.Image
Maks *ebiten.Image
MaksDest *ebiten.Image
Angle float64
Pos Coordinates
Origin Coordinates
Action HeroAction
cycles int
rotating bool
Toggled bool
Hit bool
Touched bool
dyingcount int
}
func NewHero() *Hero {
m := &Hero{
Sprite: ebiten.NewImage(48, 48),
Maks: ebiten.NewImage(48, 48),
MaksDest: ebiten.NewImage(48, 48),
Action: HeroActionDefault,
cycles: 4,
Angle: 0,
rotating: false,
Toggled: false,
dyingcount: 0,
}
m.Maks.Fill(color.White)
return m
}
func (m *Hero) ToggleRotate() {
m.rotating = !m.rotating
}
func (m *Hero) SetAngle(a float64) {
m.Angle = a
}
func (m *Hero) SetOrigin(coords Coordinates) {
m.Origin = coords
m.Pos = coords
}
func (m *Hero) Draw() {
m.Sprite.Clear()
m.MaksDest.Clear()
idx := (m.cycles / 8) % 4
y0 := 0
y1 := 48
x0 := 48 * idx
x1 := x0 + 48
switch m.Action {
case HeroActionDefault:
m.Sprite.DrawImage(heroImage.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil)
default:
}
}
func (m *Hero) Update() {
m.cycles++
}
func (m *Hero) SetHit() {
m.Action++ // = (m.Action + 1) % HeroActionMax
}
func (m *Hero) ToggleColor() {
//m.Toggled = !m.Toggled
if m.Action == HeroActionDefault {
m.Action = HeroActionDamaged
} else if m.Action == HeroActionDamaged {
m.Action = HeroActionDefault
}
}

BIN
hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -13,7 +13,7 @@ const (
) )
func main() { func main() {
ver := "Mover Test v0.05" ver := "survive v0.08"
fmt.Println(ver) fmt.Println(ver)