3 Commits

19 changed files with 281 additions and 75 deletions

Binary file not shown.

13
assets/shaders/row.kg Normal file
View File

@@ -0,0 +1,13 @@
//kage:unit pixels
package main
var Alife int
func Fragment(_ vec4, pos vec2, _ vec4) vec4 {
if Alife == 1 {
return vec4(0.0)
}
return vec4(1.0)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -26,6 +26,7 @@ type Config struct {
StateGrid *Grid // a grid from a statefile
Wrap bool // wether wraparound mode is in place or not
ShowVersion bool
UseShader bool // to use a shader to render alife cells
// for internal profiling
ProfileFile string
@@ -178,6 +179,7 @@ func ParseCommandline() (*Config, error) {
pflag.BoolVarP(&config.Invert, "invert", "i", false, "invert colors (dead cell: black)")
pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution tracks")
pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode")
pflag.BoolVarP(&config.UseShader, "use-shader", "k", false, "use shader for cell rendering")
pflag.StringVarP(&config.ProfileFile, "profile-file", "", "", "enable profiling")
pflag.BoolVarP(&config.ProfileDraw, "profile-draw", "", false, "profile draw method (default false)")

3
go.mod
View File

@@ -13,7 +13,10 @@ require (
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.7.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/tinne26/etxt v0.0.8 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)

6
go.sum
View File

@@ -6,15 +6,21 @@ github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc=
github.com/tinne26/etxt v0.0.8/go.mod h1:QM/hlNkstsKC39elTFNKAR34xsMb9QoVosf+g9wlYxM=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

99
loader-fonts.go Normal file
View File

@@ -0,0 +1,99 @@
package main
import (
"log"
"github.com/golang/freetype/truetype"
"github.com/tinne26/etxt"
"golang.org/x/image/font"
)
var FontRenderer = LoadFonts("assets/fonts")
const (
GameFont string = "NotoSans-Regular"
FontSizeBig int = 48
FontSizeNormal int = 24
FontSizeSmall int = 12
)
type Texter struct {
Renderer *etxt.Renderer
FontNormal *font.Face
FontBig *font.Face
FontSmall *font.Face
}
func LoadFonts(dir string) Texter {
fontbytes, err := assetfs.ReadFile(dir + "/" + GameFont + ".ttf")
if err != nil {
log.Fatal(err)
}
gamefont, err := truetype.Parse(fontbytes)
if err != nil {
log.Fatal(err)
}
gameface := truetype.NewFace(gamefont, &truetype.Options{
Size: float64(FontSizeNormal),
DPI: 72,
Hinting: font.HintingFull,
})
biggameface := truetype.NewFace(gamefont, &truetype.Options{
Size: float64(FontSizeBig),
DPI: 72,
Hinting: font.HintingFull,
})
smallgameface := truetype.NewFace(gamefont, &truetype.Options{
Size: float64(FontSizeSmall),
DPI: 72,
Hinting: font.HintingFull,
})
fontlib := etxt.NewFontLibrary()
_, _, err = fontlib.ParseEmbedDirFonts(dir, assetfs)
if err != nil {
log.Fatalf("Error while loading fonts: %s", err.Error())
}
if !fontlib.HasFont(GameFont) {
log.Fatal("missing font: " + GameFont)
}
err = fontlib.EachFont(checkMissingRunes)
if err != nil {
log.Fatal(err)
}
renderer := etxt.NewStdRenderer()
glyphsCache := etxt.NewDefaultCache(10 * 1024 * 1024) // 10MB
renderer.SetCacheHandler(glyphsCache.NewHandler())
renderer.SetFont(fontlib.GetFont(GameFont))
return Texter{
Renderer: renderer,
FontNormal: &gameface,
FontBig: &biggameface,
FontSmall: &smallgameface,
}
}
// helper function used with FontLibrary.EachFont to make sure
// all loaded fonts contain the characters or alphabet we want
func checkMissingRunes(name string, font *etxt.Font) error {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const symbols = "0123456789 .,;:!?-()[]{}_&#@"
missing, err := etxt.GetMissingRunes(font, letters+symbols)
if err != nil {
return err
}
if len(missing) > 0 {
log.Fatalf("Font '%s' missing runes: %s", name, string(missing))
}
return nil
}

49
loader-shaders.go Normal file
View File

@@ -0,0 +1,49 @@
package main
import (
"bytes"
"log"
"log/slog"
"path"
"strings"
"github.com/hajimehoshi/ebiten/v2"
)
type ShaderRegistry map[string]*ebiten.Shader
var Shaders = LoadShaders("assets/shaders")
func LoadShaders(dir string) ShaderRegistry {
shaders := ShaderRegistry{}
entries, err := assetfs.ReadDir(dir)
if err != nil {
log.Fatalf("failed to read shaders dir %s: %s", dir, err)
}
for _, file := range entries {
path := path.Join(dir, file.Name())
fd, err := assetfs.Open(path)
if err != nil {
log.Fatalf("failed to open shader file %s: %s", file.Name(), err)
}
defer fd.Close()
name := strings.TrimSuffix(file.Name(), ".kg")
buf := new(bytes.Buffer)
buf.ReadFrom(fd)
shader, err := ebiten.NewShader([]byte(buf.Bytes()))
if err != nil {
log.Fatal(err)
}
shaders[name] = shader
slog.Debug("loaded shader asset", "path", path)
}
return shaders
}

69
loader-sprites.go Normal file
View File

@@ -0,0 +1,69 @@
package main
import (
"embed"
"image"
_ "image/png"
"io/fs"
"log"
"path"
"strings"
"github.com/hajimehoshi/ebiten/v2"
)
// Maps image name to image data
type AssetRegistry map[string]*ebiten.Image
// A helper to pass the registry easier around
type assetData struct {
Registry AssetRegistry
}
//go:embed assets/sprites/*.png assets/fonts/*.ttf assets/shaders/*.kg
var assetfs embed.FS
// Called at build time, creates the global asset and animation registries
var Assets = LoadImages("assets/sprites")
// load pngs and json files
func LoadImages(dir string) AssetRegistry {
Registry := AssetRegistry{}
// we use embed.FS to iterate over all files in ./assets/
entries, err := assetfs.ReadDir(dir)
if err != nil {
log.Fatalf("failed to read assets dir %s: %s", dir, err)
}
for _, imagefile := range entries {
path := path.Join(dir, imagefile.Name())
fd, err := assetfs.Open(path)
if err != nil {
log.Fatalf("failed to open file %s: %s", imagefile.Name(), err)
}
defer fd.Close()
switch {
case strings.HasSuffix(path, ".png"):
name, image := ReadImage(imagefile, fd)
Registry[name] = image
}
}
return Registry
}
func ReadImage(imagefile fs.DirEntry, fd fs.File) (string, *ebiten.Image) {
name := strings.TrimSuffix(imagefile.Name(), ".png")
img, _, err := image.Decode(fd)
if err != nil {
log.Fatalf("failed to decode image %s: %s", imagefile.Name(), err)
}
image := ebiten.NewImageFromImage(img)
return name, image
}

40
main.go
View File

@@ -5,7 +5,6 @@ import (
"log"
"os"
"runtime/pprof"
"time"
_ "net/http/pprof"
@@ -26,7 +25,8 @@ func main() {
game := NewGame(config, Play)
if config.ProfileFile != "" {
// enable cpu profiling and use fake game loop
// enable cpu profiling. Do NOT use q to stop the game but
// close the window to get a profile
fd, err := os.Create(config.ProfileFile)
if err != nil {
log.Fatal(err)
@@ -35,13 +35,6 @@ func main() {
pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
Ebitfake(game)
pprof.StopCPUProfile()
fd.Close()
os.Exit(0)
}
// main loop
@@ -49,32 +42,3 @@ func main() {
log.Fatal(err)
}
}
// fake game loop, required to be able to profile the program using
// pprof. Otherwise any kind of program exit leads to an empty profile
// file.
func Ebitfake(game *Game) {
screen := ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
var loops int64
for {
err := game.Update()
if err != nil {
log.Fatal(err)
}
if game.Config.ProfileDraw {
game.Draw(screen)
}
fmt.Print(".")
time.Sleep(16 * time.Millisecond) // around 60 TPS
if loops >= game.Config.ProfileMaxLoops {
break
}
loops++
}
}

View File

@@ -35,7 +35,7 @@ type ScenePlay struct {
TicksElapsed int // tick counter for game speed
Tiles Images // pre-computed tiles for dead and alife cells
Camera Camera // for zoom+move
World *ebiten.Image // actual image we render to
World, Cache *ebiten.Image // actual image we render to
WheelTurned bool // when user turns wheel multiple times, zoom faster
Dragging bool // middle mouse is pressed, move canvas
LastCursorPos []int // used to check if the user is dragging
@@ -184,14 +184,14 @@ func (scene *ScenePlay) CheckInput() {
scene.Paused = true // drawing while running makes no sense
}
if ebiten.IsKeyPressed(ebiten.KeyPageDown) {
if scene.Config.TPG < 120 {
scene.Config.TPG++
if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
if scene.TPG < 120 {
scene.TPG++
}
}
if ebiten.IsKeyPressed(ebiten.KeyPageUp) {
if scene.TPG > 1 {
if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
if scene.TPG >= 1 {
scene.TPG--
}
}
@@ -258,25 +258,9 @@ func (scene *ScenePlay) CheckDraggingInput() {
// Zoom
_, dy := ebiten.Wheel()
step := 1
if scene.WheelTurned {
// if keep scrolling the wheel, zoom faster
step = 50
} else {
scene.WheelTurned = false
}
if dy < 0 {
if scene.Camera.ZoomFactor > -2400 {
scene.Camera.ZoomFactor -= step
}
}
if dy > 0 {
if scene.Camera.ZoomFactor < 2400 {
scene.Camera.ZoomFactor += step
}
if dy != 0 {
scene.Camera.ZoomFactor += (int(dy) * 5)
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
@@ -411,11 +395,8 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
// a nice grey grid with grid lines
op := &ebiten.DrawImageOptions{}
if scene.Config.NoGrid {
scene.World.Fill(scene.White)
} else {
scene.World.Fill(scene.Grey)
}
op.GeoM.Translate(0, 0)
scene.World.DrawImage(scene.Cache, op)
for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ {
@@ -428,6 +409,7 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
case 1:
if age > 50 && scene.Config.ShowEvolution {
scene.World.DrawImage(scene.Tiles.Old, op)
} else {
scene.World.DrawImage(scene.Tiles.Black, op)
}
@@ -443,8 +425,6 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
default:
scene.World.DrawImage(scene.Tiles.Age4, op)
}
} else {
scene.World.DrawImage(scene.Tiles.White, op)
}
}
}
@@ -475,11 +455,11 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
paused = "-- paused --"
}
ebitenutil.DebugPrint(
screen,
fmt.Sprintf("FPS: %0.2f, TPG: %d, Mem: %0.2f MB, Generations: %d %s",
ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations, paused),
)
debug := fmt.Sprintf("FPS: %0.2f, TPG: %d, Mem: %0.2f MB, Generations: %d %s",
ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations, paused)
ebitenutil.DebugPrint(screen, debug)
fmt.Println(debug)
}
}
@@ -505,6 +485,25 @@ func (scene *ScenePlay) InitPattern() {
}
}
func (scene *ScenePlay) InitCache() {
op := &ebiten.DrawImageOptions{}
if scene.Config.NoGrid {
scene.Cache.Fill(scene.White)
} else {
scene.Cache.Fill(scene.Grey)
}
for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ {
op.GeoM.Reset()
op.GeoM.Translate(float64(x*scene.Config.Cellsize), float64(y*scene.Config.Cellsize))
scene.Cache.DrawImage(scene.Tiles.White, op)
}
}
}
func (scene *ScenePlay) InitGrid(grid *Grid) {
if grid != nil {
// use pre-loaded grid
@@ -599,10 +598,12 @@ func (scene *ScenePlay) Init() {
}
scene.World = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight)
scene.Cache = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight)
scene.InitTiles()
scene.InitCache()
scene.InitGrid(grid)
scene.InitPattern()
scene.InitTiles()
scene.Index = 0
scene.TicksElapsed = 0