• Posted on

    GodotCon Amsterdam 2026

    In April, the Godot foundation organized a conference close to their home base: GodotCon Amsterdam. It was a good occasion for us to showcase Paradox Tower and get some beta feedback from other developers.

    A park with a river in Amsterdam
    Amsterdam North, under the sun

    Most importantly it was two entire days of talks about game development in Godot. Here are some of the ones I recommend checking out if you are into it.

    1000 rigged AnimationPlayers in 3D, impossible?

    Having a lot of animated 3D objects in Godot can seriously damage performance. This is something to look for if you’re developing for systems with limited resources, which may rely on the Compatibility renderer (like Android or Switch).

    Bertrand Bordage explains how to get around this limitation using Vertex Animation Textures. It basically consists of baking an animation into a shader.

    I had previously never heard of it and this talk really seems like a doable solution for Switch ports of 3D games.

    The Clone Wars: Defending Godot Games From Reupload Scams

    Godot games are very easy to reverse engineer. Many developers assume this is not an issue and will only be used by modders and curious players. Sadly, there’s a cottage industry of shady publishers who will take advantage of this to copy your game.

    I’ve previously talked a bit about technical countermeasures you can use to make this process harder. In this talk, Yasen Dinkov also explores the legal and marketing aspects of the problem. He provides ideas that can make taking down illegal store pages much easier.

    From model to gameplay in seconds: setting up your Blender Godot pipeline

    Doing a lot of manual steps can kill your productivity. A big source of this is manually importing 3D assets and setting them up as Godot scenes.

    Godot comes with a set of tools to speed that up: post-import scripts. Here, Marion Allard explains how to make the best use of it, with step-by-step examples using Blender objects:

    Make your own fractal!

    A more relaxed talk to finish, with a short and fun introduction to fractals by Marta Imke. It explores visual math and the different ways you can build fractal images out of simple functions.

    This is definitely something that can be useful if you want to create some trippy visual effects.

  • Posted on

    Steam nerfed popular upcoming. So what?

    In the latest Steam client update, Valve unveiled major changes to its store’s home page. One of the main difference is that the “Popular Upcoming” section, which shows upcoming games now shows a lot fewer titles. This seems to worry indie developers who relied on this section as a major driver of visibility before launch. I personally think this is will not change much to how indie games are marketed and might even be a positive for many niche developers.

    Steam's new popular upcoming page

    Popular upcoming is dead

    Game development is more accessible than ever, and anyone can post their games on Steam and Epic. All those new developers needing to market their games has created a cottage industry of game marketing influencers. The strategy they teach is basically always a variant of:

    1. Make a game (and a demo)
    2. Get a lot of people to play the demo during Steam Next Fest
    3. Get 6000+ wishlists
    4. Appear in Popular Upcoming
    5. ???
    6. Profit

    Since everyone is running around with approximately the same strategy, it became a very crowded trade. With demos getting more and more polished, Next Fest began to have diminishing returns for most developers. In the same way Popular Upcoming was very impactful when maybe 1 or 2 games appeared in it every day, but when it started showing a dozen game a day, it stopped being a guarantee of success.

    And let’s be honest, players have probably been paying less and less attention to this section of the store for a while. Unless they were really interested in finding out what every shovelware publisher has been working on.

    Steam's new personal calendar upcoming section

    Long live the Personal Calendar

    In the same update, Valve introduced a new feature: the Personal Calendar. It’s a whole page which aims at making players discover games that are match their taste.

    The recommendation algorithm seems to work somewhat decently. I do not think it is entirely based on tags, since it seems to recommend me a lot of “Open World Survival Craft” games, which is not something I usually play. It may be using wishlist behavior of similar players to make recommendations.

    One of the major differences is that it shows a calendar for the next eight weeks, so games can have a much longer visibility window. If a player of your target audience didn’t browse Steam on the day before your launch, it would miss you on popular upcoming, while now they have six weeks to do that.

    It also shows recently released games, in the past 7 days and past month. Contrary to the homepage “Popular New Releases” tab (ex “New & Trending”), which requires a lot of active players, this section seems accessible even to unknown games. Steam is currently recommending me Imago Season with (at the moment of writing) 0 review and only 2 concurrent players.

    Steam's new personal calendar past 7 days section

    A new era for niche games?

    Before this change, it was extremely difficult for developers to market niche games. I know it from first-hand experience, since I have no doubt Dice ‘n Goblins would have been easier to market if it was not the weirdest combination of ideas possible.

    Because of this, a lot of new indie game developers were advised to stick to a popular genre. You love real-time strategy? Too bad, you’ll have to make an action roguelike instead.

    This has caused a loss of creativity in the indie game world. With many developers working on genres that do not interest them, using recycled ideas and aesthetics.

    I do believe that this new version of Steam might swing the pendulum in the other way. Now that players of niche games can get recommendation customized to their taste, making games for a non-mainstream audience can become viable again.

  • Posted on

    Preview Ebitengine shaders with Luluka

    Lately, I’ve been experimenting with building games in Ebitengine. It’s a 2D engine that lets you create games using the Go programming language. To be able to iterate faster on visual effects, I have created a tool that lets me preview shaders: Luluka.

    Luluka showing a basic shader

    Kage Shaders

    For those who feel like they missed an episode, shaders are small programs running on the GPU which can be used to modify the pixels of an image. In games, they are used everywhere to control how a game looks. They can be used create special effects like blurring an image or making your screen look like a CRT from the 90s.

    Ebitengine has its own shader language called Kage. It is very convenient because it has a syntax extremely close to Go. Close enough that you can even run go fmt to format your Kage files. Under the hood, Ebitengine will automatically convert Kage shaders into a format understandable by the GPU.

    Quasilyte’s article about Ebitengine shaders is generally the most comprehensive introduction to the subject. You can also learn more about them in the Kage’s desk.

    Moria Luluka transforming into Cure Shadow Arcana in the anime Star Detective Precure
    Moria Luluka from Star Detective Precure! by Toei Animation

    Luluka

    One of the main difference between Ebitengine and other game engines like Godot, is that it doesn’t have a visual editor. This is generally fine, since I’ve spent most of my career avoiding What-You-See-Is-What-You-Get tools in favor of staying inside NeoVim.

    But for shaders, you can end up spending a lot of time tweaking a few variables until you get them to look right. That’s why I decided to build my own tool that would let me work on a shader in isolation, and quickly change the variables we give it.

    You can install Luluka using the following command:

    go install github.com/Tsukumogami-Software/[email protected]
    

    Run a shader by pointing it straight to the file, passing textures with -i and uniform values with -u:

    luluka sample/transition.kage -i image2.png -i image.png -u Steepness:80 -u Seed.0:15 -u Seed.1:100 -u Seed.2:5000 -u Seed.3:5000 -u Speed:0.08
    

    For more convenience, you can use a YAML file to pass your uniform values. This is especially practical when working with arrays or matrices, since commands can get very long:

    Steepness: 80
    Seed: [15.0, 100.0, 5000.0]
    Speed: 0.08
    
    luluka sample/transition.kage -i image2.png -i image.png -v values.yaml
    
    The luluka -h output
  • Posted on

    Protecting Godot games against reverse engineering

    Godot games are known to be easy to reverse engineer. Simple tools can extract assets and source code from the packaged files. If you are making commercial games you probably want to take some steps to avoid this.

    A key
    Photo by Jorien Loman

    Godot RE Tools

    The most popular Godot reverse engineering software is gdsdecomp aka Godot RE Tools. It’s very multi-platform, simple to use, and even comes with a GUI.

    In a few clicks, you can “Recover” a project from an executable or .pck file. Gdsdecomp is able to find back the file structure of the project, every asset you exported, and the source code with full variable and function names.

    Godot RE Tools decrypting some simple animation code

    This is actually really well-made software. It even comes with convenient utilities for people who want to patch translations (one of the many use cases of reverse engineering games).

    Encrypting .pck files

    First let’s preface this section with a warning. There’s no way to 100% guarantee that it’s going to be impossible to decompile a game, aside from never distributing the executable. The only thing we can do is make reverse engineering more difficult and time-consuming.

    Anyway, the recommended way of protecting Godot games against reverse engineering is to encrypt the files inside it. This is done using AES-256, and requires compiling custom export templates.

    You can find the details in the official docs, but the general idea is this:

    1. Clone the Godot source code:
       git clone [email protected]:godotengine/godot.git
      
    2. Generate an AES-256 key (32 bits in hex format):
       openssl rand -hex 32 > godot.gdkey
      
    3. Put this key in your environment variables:
       export SCRIPT_AES256_ENCRYPTION_KEY=$(cat godot.gdkey)
      
    4. Compile the new export templates (for wasm in this example):
       scons platform=web target=template_release && scons platform=web target=template_debug
      
    5. Set the new templates as custom templates in your export:
      Advanced options in Godot Project Export showing Custom Templates being set
    6. In your export encryption settings, check “Encrypt Exported PCK”, “Encrypt Index”, and do not forget to put the files and folders you want to encrypt in “Filters to include”:
      Encryption options in Godot Project Export showing Filters to include
    7. Generate an IV (you should use a new one every export):
       openssl rand -hex 16
      
    8. Last but not least, set your AES key and IV in the encryption settings page.

    If you did everything correctly, your exported .pck should not be readable by gdsdecomp without knowing the key. At the same time, players should still be able to play the game as usual without issues or needing to know what AES-256 means.

    Sadly, the key is stored in plain text in memory, and it is not very hard to find it there. According to an estimate I just made up, it would take a 12 years-old with an hex editor and a YouTube tutorial around 15 minutes to get the key. There’s even a tool that promises it can find it in only 50 ms.

    A sample encryption key visible in Ghidra

    Godot-Secure

    To solve this problem, we need to obfuscate the decryption process a little bit. For those that feel this sounds terribly complicated, there’s a script called Godot-Secure that was built to help you with that.

    It will modify Godot source code to significantly alter the decryption process. Instead of directly using the key we store in memory, it will use it and a secret token to derivate a second key and decipher the files with it. In addition, to that, it will change a few magic numbers and can switch the algorithm from AES-256 to Camellia-256.

    Once you have run the script, you will need to recompile both the export templates and the editor. This is because the editor is responsible for encrypting the files during export.

    Gdsdecomp failing to decompile a project

    After exporting a file with the secured Godot, the attackers can still easily obtain our key from the binary files. However, this key is useless by itself. They will also need to find the secret token, work through the key derivation method and re-implement the decryption algorithm. This can take a lot of time and requires actual programming knowledge.

    Improving the obfuscation

    Of course, if you want to play around with some C++, you can make this a bit more robust by adding custom logic of your own. There are two files that will be relevant to you.

    First one is core/io/file_access_encrypted.cpp. It contains the encryption logic in the function FileAccessEncrypted::open_and_parse and the decryption logic in the function FileAccessEncrypted::_close.

    CryptoCore::AESContext ctx;
    
    ctx.set_encode_key(key.ptrw(), 256); // Due to the nature of CFB, same key schedule is used for both encryption and decryption!
    ctx.decrypt_cfb(ds, iv.ptrw(), data.ptrw(), data.ptrw());
    

    The second is core/io/file_access_pack.cpp. This one contains how the key is loaded from memory in the PackedSourcePCK::try_open_pack function and FileAccessPack constructor. Be careful when modifying this part, as changes with how you load the key will need to be reflected in how you set the key from the editor.

    Vector<uint8_t> key;
    #ifdef TOOLS_ENABLED
    if (!p_decryption_key.is_empty()) {
    	ERR_FAIL_COND_MSG(p_decryption_key.size() != 32, "Decryption key must be 256-bit.");
    	constexpr uint8_t empty_key[32] = {};
    	if (memcmp(script_encryption_key, empty_key, sizeof(empty_key)) == 0) {
    		key = p_decryption_key;
    	}
    } else
    #endif
    {
    	key.resize(32);
    	memcpy(key.ptrw(), script_encryption_key, 32);
    }
    

    The script_encryption_key variable itself is set at compile time by the script core/core_builders.py.

  • Posted on

    Making games in Go with Ebitengine

    Like a lot of people, you may be using Go at work to develop backend applications or create infrastructure tools. But you may not be aware that you can actually make video games in Go using Ebitengine.

    A bowl of fried shrimps
    Photo by gomding

    Just enough to make games

    Ebitengine aims at being dead simple. It was explicitly designed to have the most minimalistic API possible and still let people make the games they want. Of course, it has some limitations, for example it doesn’t handle 3D (at least, not officially). But it is still functional enough that many commercial indie games have been made with it.

    Switching to Ebitengine from Unity or Godot is a bit like switching from Spring Framework to a micro-framework like Gin or Chi. At first, you’ll feel like you’re spending all your time re-inventing the wheel, but when you start getting used to it, you realize that the increased flexibility and reliability makes your life easier in the long run.

    Graphics

    The core of Ebitengine is its ability to display and manipulate images. The screen itself is considered an image, on which we are going to draw the rest of our game. Each time we display an image using DrawImage, we can give it options. Those allow us to change its position, scale, rotation, color or filtering.

    Source code
    package main
    
    import (
    	"bytes"
    	_ "embed"
    	"image/png"
    	"log"
    	"math/rand"
    
    	"github.com/hajimehoshi/ebiten/v2"
    )
    
    var game *Game
    
    //go:embed logo.png
    var logoFile []byte
    
    const (
    	maxSpeed = 3
    	minSpeed = 1
    	scale    = 2
    )
    
    func randomColor() ebiten.ColorScale {
    	scale := ebiten.ColorScale{}
    	scale.Scale(
    		rand.Float32(),
    		rand.Float32(),
    		rand.Float32(),
    		1,
    	)
    	return scale
    }
    
    func randomSpeed() float64 {
    	return minSpeed + rand.Float64()*(maxSpeed-minSpeed)
    }
    
    // Game implements ebitengine Game interface and represents our game loop
    type Game struct {
    	image     *ebiten.Image
    	geometry  ebiten.GeoM
    	direction [2]float64
    	color     ebiten.ColorScale
    	width     int
    	height    int
    }
    
    // Update is called every frame to update the current game state
    func (g *Game) Update() error {
    	g.geometry.Translate(
    		g.direction[0],
    		g.direction[1],
    	)
    
    	// bounce and change color if out of screen
    	x := g.geometry.Element(0, 2)
    	x2 := x + float64(g.image.Bounds().Dx())*scale
    	if x < 0 {
    		g.direction[0] = randomSpeed()
    		g.color = randomColor()
    	} else if x2 > float64(g.width) {
    		g.direction[0] = -1 * randomSpeed()
    		g.color = randomColor()
    	}
    
    	y := g.geometry.Element(1, 2)
    	y2 := y + float64(g.image.Bounds().Dy())*scale
    	if y < 0 {
    		g.direction[1] = randomSpeed()
    		g.color = randomColor()
    	} else if y2 > float64(g.height) {
    		g.direction[1] = -1 * randomSpeed()
    		g.color = randomColor()
    	}
    
    	return nil
    }
    
    // Draw is called every frame to display images on the screen
    func (g *Game) Draw(screen *ebiten.Image) {
    	screen.DrawImage(g.image, &ebiten.DrawImageOptions{
    		GeoM:       game.geometry,
    		ColorScale: g.color,
    	})
    }
    
    // Layout is called every frame to indicate the window/screen size
    func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
    	g.width = outsideWidth
    	g.height = outsideHeight
    	return outsideWidth, outsideHeight
    }
    
    func init() {
    	reader := bytes.NewReader(logoFile)
    	png, err := png.Decode(reader)
    	if err != nil {
    		log.Panicf("Failed to decode image:\n%v", err)
    	}
    	image := ebiten.NewImageFromImage(png)
    
    	geom := ebiten.GeoM{}
    	geom.Scale(2, 2)
    
    	direction := [2]float64{
    		randomSpeed(),
    		randomSpeed(),
    	}
    	color := randomColor()
    
    	game = &Game{
    		image:     image,
    		geometry:  geom,
    		direction: direction,
    		color:     color,
    	}
    }
    
    func main() {
    	if err := ebiten.RunGame(game); err != nil {
    		log.Fatal(err)
    	}
    }
    


    Input

    Like any proper engine, Ebitengine helps you handle player input. Basic functions like IsKeyPressed or IsMouseButtonPressed are directly found inside the main ebiten module. More advanced functions are placed in the inpututil module. Those can be very useful if you need to work with controllers or touchscreens.

    Source code (main.go)
    package main
    
    import (
    	"bytes"
    	_ "embed"
    	"image/color"
    	"image/png"
    	"log"
    
    	"github.com/hajimehoshi/ebiten/v2"
    	"github.com/hajimehoshi/ebiten/v2/inpututil"
    )
    
    var game *Game
    
    const (
    	screenWidth  = 160
    	screenHeight = 240
    )
    
    // Game is our top level structure
    type Game struct {
    	egg    *Egg
    	nests  []*Nest
    	scroll float64
    
    	touchIDs []ebiten.TouchID
    }
    
    // Update handles the user input, movement and scrolling
    func (g *Game) Update() error {
    	if g.scroll > 0 {
    		g.HandleScrolling()
    		return nil // the rest of the game logic is blocked during scrolling
    	}
    
    	g.touchIDs = inpututil.AppendJustPressedTouchIDs(g.touchIDs[:0])
    	if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) ||
    		inpututil.IsKeyJustPressed(ebiten.KeySpace) ||
    		len(g.touchIDs) != 0 {
    		g.egg.Jump()
    	}
    
    	g.egg.UpdatePosition()
    	for _, nest := range g.nests {
    		nest.UpdatePosition()
    		if g.egg.IsFalling() {
    			if nest.CheckLanding(g.egg) {
    				g.nests = append(g.nests, NewMovingNest())
    				g.scroll = screenHeight / 3
    			}
    		}
    	}
    
    	return nil
    }
    
    // HandleScrolling smoothly moves the elements by a third of the screen and removes old nests
    func (g *Game) HandleScrolling() {
    	distance := float64(screenHeight) / 30
    	g.scroll -= distance
    	g.egg.Scroll(distance)
    
    	visibleNests := []*Nest{}
    	for _, nest := range g.nests {
    		nest.Scroll(distance)
    		if !nest.IsOutOfScreen() {
    			visibleNests = append(visibleNests, nest)
    		}
    	}
    	g.nests = visibleNests
    }
    
    // Draw displays the background, egg and nests
    func (g *Game) Draw(screen *ebiten.Image) {
    	screen.Fill(color.RGBA{
    		R: 77,
    		G: 186,
    		B: 233,
    		A: 255,
    	})
    	g.egg.Draw(screen)
    	for _, nest := range g.nests {
    		nest.Draw(screen)
    	}
    }
    
    // Layout returns a constant screen width and height
    func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
    	return screenWidth, screenHeight
    }
    
    func loadImage(file []byte) *ebiten.Image {
    	reader := bytes.NewReader(file)
    	png, err := png.Decode(reader)
    	if err != nil {
    		log.Panicf("Failed to decode image:\n%v", err)
    	}
    	return ebiten.NewImageFromImage(png)
    }
    
    func init() {
    	eggImage = loadImage(eggFile)
    	nestImage = loadImage(nestFile)
    	game = &Game{
    		egg:      NewEgg(),
    		nests:    []*Nest{NewBottomNest(), NewTopNest()},
    		touchIDs: []ebiten.TouchID{},
    		scroll:   0,
    	}
    }
    
    func main() {
    	if err := ebiten.RunGame(game); err != nil {
    		log.Fatal(err)
    	}
    }
    
    Source code (egg.go)
    package main
    
    import (
    	_ "embed"
    
    	"github.com/hajimehoshi/ebiten/v2"
    )
    
    const (
    	eggWidth  = 16
    	eggHeight = 16
    )
    
    //go:embed egg.png
    var eggFile []byte
    var eggImage *ebiten.Image
    
    // Egg represents our player
    type Egg struct {
    	geom     ebiten.GeoM
    	velocity float64
    	gravity  float64
    }
    
    // NewEgg creates an egg at the beginning of the game
    func NewEgg() *Egg {
    	eggGeom := ebiten.GeoM{}
    	eggGeom.Translate(
    		screenWidth/2-eggWidth/2,
    		screenHeight*2/3,
    	)
    	return &Egg{
    		geom:     eggGeom,
    		velocity: 0,
    		gravity:  0,
    	}
    }
    
    // Draw draws the egg sprite on the screen
    func (e *Egg) Draw(screen *ebiten.Image) {
    	screen.DrawImage(eggImage, &ebiten.DrawImageOptions{
    		GeoM: e.geom,
    	})
    }
    
    // Jump triggers the start of a jump
    func (e *Egg) Jump() {
    	if e.velocity < 0 {
    		return // we're already jumping
    	}
    	e.velocity = -10
    	e.gravity = 0.5
    }
    
    // UpdatePosition moves the egg during jumps / falls
    func (e *Egg) UpdatePosition() {
    	e.velocity += e.gravity
    	e.geom.Translate(
    		0,
    		e.velocity,
    	)
    }
    
    // GetPosition returns the egg X/Y
    func (e *Egg) GetPosition() (float64, float64) {
    	eggX := e.geom.Element(0, 2)
    	eggY := e.geom.Element(1, 2)
    	return eggX, eggY
    }
    
    // Stop cancels any jump / fall
    func (e *Egg) Stop() {
    	e.gravity = 0
    	e.velocity = 0
    }
    
    // IsFalling returns true if the egg is currently falling
    func (e *Egg) IsFalling() bool {
    	return e.velocity > 0
    }
    
    // Scroll moves the egg during scrollig
    func (e *Egg) Scroll(distance float64) {
    	e.geom.Translate(0, distance)
    }
    
    Source code (nest.go)
    package main
    
    import (
    	_ "embed"
    	"math/rand"
    
    	"github.com/hajimehoshi/ebiten/v2"
    )
    
    const (
    	nestWidth  = 32
    	nestHeight = 16
    )
    
    //go:embed nest.png
    var nestFile []byte
    var nestImage *ebiten.Image
    
    // Nest represents the platforms
    type Nest struct {
    	geom     ebiten.GeoM
    	landed   bool
    	velocity float64
    }
    
    // NewBottomNest creates the starting point
    func NewBottomNest() *Nest {
    	nestGeom := ebiten.GeoM{}
    	nestGeom.Translate(
    		screenWidth/2-nestWidth/2,
    		screenHeight*2/3+eggHeight/2,
    	)
    	return &Nest{
    		geom:     nestGeom,
    		landed:   true,
    		velocity: 0,
    	}
    }
    
    // NewTopNest creates the second (fixed) nest
    func NewTopNest() *Nest {
    	nestGeom := ebiten.GeoM{}
    	nestGeom.Translate(
    		screenWidth/2-nestWidth/2,
    		screenHeight/3+eggHeight/2,
    	)
    	return &Nest{
    		geom:     nestGeom,
    		landed:   false,
    		velocity: 0,
    	}
    }
    
    // NewMovingNest creates a moving platform
    func NewMovingNest() *Nest {
    	nestGeom := ebiten.GeoM{}
    	nestGeom.Translate(
    		screenWidth/2-nestWidth/2,
    		0,
    	)
    	velocity := 0.5 + rand.Float64()
    	if rand.Intn(2) == 1 {
    		velocity *= -1
    	}
    	return &Nest{
    		geom:     nestGeom,
    		landed:   false,
    		velocity: velocity,
    	}
    }
    
    // Draw displays the nest on the screen
    func (n *Nest) Draw(screen *ebiten.Image) {
    	screen.DrawImage(nestImage, &ebiten.DrawImageOptions{
    		GeoM: n.geom,
    	})
    }
    
    // UpdatePosition moves the nest
    func (n *Nest) UpdatePosition() {
    	n.geom.Translate(n.velocity, 0)
    	x := n.geom.Element(0, 2)
    	if x < 0 || x+nestWidth > screenWidth {
    		n.velocity *= -1
    	}
    }
    
    // CheckLanding returns true if the egg has landed in a new nest
    func (n *Nest) CheckLanding(egg *Egg) bool {
    	eggX, eggY := egg.GetPosition()
    	nestX := n.geom.Element(0, 2)
    	nestY := n.geom.Element(1, 2)
    	if eggX >= nestX &&
    		eggX+eggWidth <= nestX+nestWidth &&
    		eggY+eggHeight >= nestY+5 &&
    		eggY+eggHeight <= nestY+15 {
    		// landing in a nest
    		egg.Stop()
    		if !n.landed {
    			// we landed in a new nest
    			n.velocity = 0
    			n.landed = true
    			return true
    		}
    		return false
    	}
    	return false
    }
    
    // Scroll moves the nest during scrolling
    func (n *Nest) Scroll(distance float64) {
    	n.geom.Translate(0, distance)
    }
    
    // IsOutOfScreen returns true if the nest is not displayed anymore
    func (n *Nest) IsOutOfScreen() bool {
    	y := n.geom.Element(1, 2)
    	return y > screenHeight
    }
    


    Audio

    Ebitengine’s audio module contains everything you need to play sound effects or music. On top of providing the usual Play/Pause/Rewind functions, it handles decoding of mp3, ogg, and wav audio files.

    Low-level management of audio stream is available through the oto library, which is also part of the Ebitengine project.

    Source code (sample.go)
    package main
    
    import (
    	"bytes"
    	"embed"
    	"image/png"
    	"io/fs"
    	"log"
    	"path/filepath"
    
    	"github.com/hajimehoshi/ebiten/v2"
    	"github.com/hajimehoshi/ebiten/v2/audio"
    	"github.com/hajimehoshi/ebiten/v2/audio/wav"
    )
    
    const (
    	sampleWidth  = 36
    	sampleHeight = 36
    )
    
    //go:embed images
    var images embed.FS
    
    //go:embed sounds
    var sounds embed.FS
    
    // Sample represents a sound sample and its icon
    type Sample struct {
    	image  *ebiten.Image
    	geom   ebiten.GeoM
    	player *audio.Player
    }
    
    // IsTargeted returns true if the mouse or touch position is on the sample
    func (s *Sample) IsTargeted(x, y int) bool {
    	sampleX := int(s.geom.Element(0, 2))
    	sampleY := int(s.geom.Element(1, 2))
    	return x >= sampleX &&
    		x < sampleX+sampleWidth &&
    		y >= sampleY &&
    		y < sampleY+sampleWidth
    }
    
    // Play rewinds and plays the sample
    func (s *Sample) Play() {
    	err := s.player.Rewind()
    	if err != nil {
    		log.Panicf("Failed to rewind player: %v", err)
    	}
    	s.player.Play()
    }
    
    // Draw displays the sample icon
    func (s *Sample) Draw(screen *ebiten.Image) {
    	screen.DrawImage(s.image, &ebiten.DrawImageOptions{
    		GeoM: s.geom,
    	})
    }
    
    func createPlayer(context *audio.Context, filename string) *audio.Player {
    	file, err := fs.ReadFile(
    		sounds,
    		filepath.Join("sounds", filename),
    	)
    	if err != nil {
    		log.Panicf("Failed to read embedded sounds fs: %v", err)
    	}
    	reader := bytes.NewReader(file)
    
    	stream, err := wav.DecodeWithSampleRate(sampleRate, reader)
    	if err != nil {
    		log.Panicf("Failed to decode sample: %v", err)
    	}
    
    	player, err := context.NewPlayer(stream)
    	if err != nil {
    		log.Panicf("Failed to create player: %v", err)
    	}
    	return player
    }
    
    func loadImage(filename string) *ebiten.Image {
    	file, err := fs.ReadFile(
    		images,
    		filepath.Join("images", filename),
    	)
    	if err != nil {
    		log.Panicf("Failed to read embedded images fs: %v", err)
    	}
    	reader := bytes.NewReader(file)
    
    	png, err := png.Decode(reader)
    	if err != nil {
    		log.Panicf("Failed to decode image:\n%v", err)
    	}
    	return ebiten.NewImageFromImage(png)
    }
    
    // Skull creates our skull sample (top left)
    func Skull(context *audio.Context) *Sample {
    	return &Sample{
    		image:  loadImage("skull.png"),
    		player: createPlayer(context, "skull.wav"),
    		geom:   ebiten.GeoM{},
    	}
    }
    
    // Alert creates our alert sample (top right)
    func Alert(context *audio.Context) *Sample {
    	geom := ebiten.GeoM{}
    	geom.Translate(36, 0)
    	return &Sample{
    		image:  loadImage("alert.png"),
    		player: createPlayer(context, "alert.wav"),
    		geom:   geom,
    	}
    }
    
    // Question creates our question sample (bottom left)
    func Question(context *audio.Context) *Sample {
    	geom := ebiten.GeoM{}
    	geom.Translate(0, 36)
    	return &Sample{
    		image:  loadImage("question.png"),
    		player: createPlayer(context, "question.wav"),
    		geom:   geom,
    	}
    }
    
    // Heart creates our heart sample (bottom right)
    func Heart(context *audio.Context) *Sample {
    	geom := ebiten.GeoM{}
    	geom.Translate(36, 36)
    	return &Sample{
    		image:  loadImage("heart.png"),
    		player: createPlayer(context, "heart.wav"),
    		geom:   geom,
    	}
    }
    
    Source code (main.go)
    package main
    
    import (
    	"log"
    
    	"github.com/hajimehoshi/ebiten/v2"
    	"github.com/hajimehoshi/ebiten/v2/audio"
    	"github.com/hajimehoshi/ebiten/v2/inpututil"
    )
    
    const (
    	screenWidth  = 72
    	screenHeight = 72
    	sampleRate   = 44100
    )
    
    var game *Game
    
    // Game contains a collection of samples
    type Game struct {
    	samples []*Sample
    }
    
    // Update plays a sample when it is clicked or touched
    func (g *Game) Update() error {
    	if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
    		for _, sample := range g.samples {
    			if sample.IsTargeted(ebiten.CursorPosition()) {
    				sample.Play()
    			}
    		}
    	}
    
    	touchIDs := inpututil.AppendJustPressedTouchIDs(nil)
    	for _, touchID := range touchIDs {
    		for _, sample := range g.samples {
    			if sample.IsTargeted(ebiten.TouchPosition(touchID)) {
    				sample.Play()
    			}
    		}
    	}
    	return nil
    }
    
    // Draw displays every sample's icon
    func (g *Game) Draw(screen *ebiten.Image) {
    	for _, sample := range g.samples {
    		sample.Draw(screen)
    	}
    }
    
    // Layout returns a fixed 72 * 72 layout
    func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
    	return screenWidth, screenHeight
    }
    
    func init() {
    	context := audio.NewContext(sampleRate)
    	game = &Game{
    		samples: []*Sample{
    			Skull(context),
    			Alert(context),
    			Question(context),
    			Heart(context),
    		},
    	}
    }
    
    func main() {
    	if err := ebiten.RunGame(game); err != nil {
    		log.Fatal(err)
    	}
    }
    


    Shaders

    Go code usually runs on the CPU. Sometimes if your game needs faster processing for animations or effects, you may want to use shaders. Shaders are small pieces of software that will be executed on the GPU. This allows you to execute a function over every pixel of an image in parallel.

    Ebitengine comes with its own shader language called Kage. It is very similar to Go. So much, that syntax highlighting tools that work with Go should not have any problems working with Kage. If you want to learn more about this, I recommend checking out tinne26’s Kage’s desk

    Source code (main.go)
    package main
    
    import (
    	_ "embed"
    	"log"
    	"time"
    
    	"github.com/hajimehoshi/ebiten/v2"
    )
    
    //go:embed shader.kage
    var shaderFile []byte
    
    const (
    	screenWidth  = 512
    	screenHeight = 512
    )
    
    var game *Game
    
    // Game contains the compiled shader and keeps track of the time
    type Game struct {
    	shader    *ebiten.Shader
    	startTime time.Time
    }
    
    // Draw displays the shader on the entire screen
    func (g *Game) Draw(screen *ebiten.Image) {
    	screen.DrawRectShader(
    		screenWidth,
    		screenHeight,
    		g.shader,
    		&ebiten.DrawRectShaderOptions{
    			Uniforms: map[string]any{
    				"Center": []float32{
    					float32(screenWidth) / 2,
    					float32(screenHeight) / 2,
    				},
    				"Time": time.Now().Sub(g.startTime).Seconds(),
    			},
    		},
    	)
    }
    
    // Update does nothing here
    func (g *Game) Update() error {
    	return nil
    }
    
    // Layout returns a fixed width/height
    func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
    	return screenWidth, screenHeight
    }
    
    func init() {
    	shader, err := ebiten.NewShader(shaderFile)
    	if err != nil {
    		log.Panicf("Failed to create shader: %f", err)
    	}
    	game = &Game{
    		shader:    shader,
    		startTime: time.Now(),
    	}
    }
    
    func main() {
    	if err := ebiten.RunGame(game); err != nil {
    		log.Fatal(err)
    	}
    }
    
    Source code (shader.kage)
    //kage:unit pixels
    package main
    
    // Center is the coordinates of the center of the screen
    var Center vec2
    
    // Time allows us to update the shader over time
    var Time float
    
    // Fragment is the main shader function, run over every pixel
    func Fragment(targetCoords vec4, sourceCoords vec2, color vec4) vec4 {
        //delta is the vector from the center to our pixel
    	delta := targetCoords.xy - Center
    	distance := length(delta)
    	angle := atan2(delta.y, delta.x)
    
        //band is going to generate alternate bands based on distance and angle
    	spiralValue := distance + Time * 50.0 - 10.0 * angle
    	band := mod(spiralValue, 63.0)
    
    	if band < 32.0 {
    		return vec4(1, 0, 0, 1) // red
    	} else {
    		return vec4(0, 0, 0, 1) // black
    	}
    }
    


    More than just PC games

    Go natively works well on Windows, Mac, and Linux. As you can see above, it can also run in your browser with WebAssembly. Thanks to some pretty clever tricks, you can actually compile go for basically any system that implements a libc. For example, you can compile Ebitengine games for the Nintendo Switch (as long as you have access to the required SDK and hardware).

    Since Ebitengine is pretty flexible, it can be used for other things than games. Guigui is a GUI Framework that makes use of it. It is currently in alpha, and is being very actively developed by the same people who made Ebitengine.

subscribe via RSS