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)
}

196
game.go
View File

@@ -1,13 +1,20 @@
package main
import (
"bytes"
"fmt"
"image"
"image/color"
"log"
"math"
"math/rand/v2"
"mover/fonts"
_ "embed"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/hajimehoshi/ebiten/v2/vector"
)
@@ -16,17 +23,31 @@ const (
MOVER_HEIGHT = 48
)
var (
tilesetImage *ebiten.Image
//go:embed grasstile.png
tileset_img []byte
)
type Game struct {
collisionMask *ebiten.Image
projectileMask *ebiten.Image
background *ebiten.Image
collisionMask *ebiten.Image
projectileMask *ebiten.Image
heroCollisionMask *ebiten.Image
heroCollisionCpy *ebiten.Image
Pos Coordinates
Paused bool
initialized bool
mover *Mover
gameover bool
reset bool
runtime float64
hero *Hero
projectiles map[int]*Projectile
explosion *Explosion
score int
counter int
timer int
targets []*Mover
@@ -37,19 +58,44 @@ type Game struct {
//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() {
origin := Coordinates{X: 640 / 2, Y: 480 / 2}
g.mover = NewMover()
g.mover.SetOrigin(origin)
g.mover.ToggleRotate()
g.ConstructBackground()
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.
}
@@ -71,11 +117,12 @@ func (g *Game) Update() error {
}
}
if !g.initialized {
if !g.initialized || g.reset {
g.Initialize()
g.projectiles = make(map[int]*Projectile)
g.initialized = true
g.reset = false
} else {
g.StepGame()
}
@@ -87,19 +134,32 @@ func (g *Game) Update() error {
func (g *Game) Draw(screen *ebiten.Image) {
g.mover.Draw()
screen.Clear()
screen.DrawImage(g.background, nil)
g.hero.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)
*/
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.mover.Angle)
op.GeoM.Translate(g.mover.Pos.X, g.mover.Pos.Y)
screen.DrawImage(g.mover.Sprite, op)
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()
@@ -142,10 +202,13 @@ func (g *Game) CleanupTargets() {
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)
}*/
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
}
@@ -160,20 +223,20 @@ func (g *Game) StepGame() {
g.UpdateHeroPosition()
g.mover.Update()
g.hero.Update()
g.explosion.Update()
g.UpdateTargets()
g.UpdateProjectiles()
//append new projectiles
g.AppendProjectiles()
//add new target with increasing frequency
g.AddNewTargets()
//handle pulsewave updates
g.HandlePulseWaveUpdate()
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++
@@ -198,8 +261,8 @@ func (g *Game) HandlePulseWaveUpdate() {
//check collisions
for _, target := range g.targets {
dx := target.Pos.X - g.mover.Pos.X
dy := target.Pos.Y - g.mover.Pos.Y
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 {
@@ -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 {
//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)
@@ -261,9 +326,9 @@ func (g *Game) UpdateProjectiles() {
func (g *Game) UpdateTargets() {
for _, target := range g.targets {
if !target.Hit {
dx := g.mover.Pos.X - target.Pos.X
dy := g.mover.Pos.Y - target.Pos.Y
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.
@@ -271,6 +336,30 @@ func (g *Game) UpdateTargets() {
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()
}
}
@@ -284,8 +373,8 @@ func (g *Game) ResetTargetTouches() {
func (g *Game) AppendProjectiles() {
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+1] = 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.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.)
}
}
@@ -293,14 +382,18 @@ func (g *Game) HandleInput() {
if len(g.gamepadIDs) > 0 {
if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton11) {
if !g.explosion.Active {
g.explosion.SetOrigin(g.mover.Pos)
g.explosion.SetOrigin(g.hero.Pos)
g.explosion.Reset()
g.explosion.ToggleActivate()
}
}
if inpututil.IsGamepadButtonJustPressed(0, ebiten.GamepadButton9) {
g.Paused = !g.Paused
if g.gameover {
g.reset = true
} else {
g.Paused = !g.Paused
}
}
//account for controller sensitivity
@@ -315,7 +408,7 @@ func (g *Game) HandleInput() {
}
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)
inpy := ebiten.GamepadAxisValue(0, 1)
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 {
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
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 (
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect
@@ -13,4 +17,5 @@ require (
github.com/jezek/xgb v1.1.1 // indirect
golang.org/x/sync v0.8.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() {
ver := "Mover Test v0.05"
ver := "survive v0.08"
fmt.Println(ver)