ForgeLeaf Logo

Consent to our cookie policy 🍪

We use a privacy-friendly, cookie-free analytics tool (Umami). Some cookies are used for functionality only. You may check our Privacy Policy.
A computer screen showing a game with a goblin and a werewolf

Handling Input in Forge 🎮

Bismillah.
This is the fourth article in the Forge series!
In the previous post, we learned how to add audio to our games using the Audio package. Today, we’ll take our interactivity to the next level by learning how to handle input in Forge.

Forge offers two main approaches to handling input events:
callbacks (event-driven) and polling (state-driven).
Both have their advantages and knowing when to use which will help you write cleaner, more responsive code.


Understanding Input in Forge ⚙️

Input handling is at the heart of every interactive application. Whether you’re making a platformer, a puzzle game, or a little experiment, you’ll be constantly reacting to user input.

Forge provides an input system that can be accessed via driver.Input.
You can either poll keys every frame or register callbacks for specific actions.

Let’s compare both methods quickly:

MethodDescriptionWhen to UseExampleDevice
PollingYou manually check key states each frame IsKeyPressed()Continuous movement or actions that need to update per frameCharacter movementKeyboard, Mouse
CallbacksForge calls your function when an event occurs SetKeyCallback()Single actions or eventsMenu navigation, quitting on ESCKeyboard, Mouse

In this tutorial, we’ll use polling to move two players on screen, one using WASD keys and another using the arrow keys. We’ll also use a callback to handle the Escape key to close our game window.

Unfortunately, I wasn’t able to show a complete example on how to use the mouse button polls and callbacks, we’ll keep that for another tutorial!


Project Setup 🧱

We’ll start from the ForgeStarterTemplate.
Clone it using:

git clone https://github.com/ForgeLeaf/ForgeStarterTemplate.git
cd ForgeStarterTemplate

Now open main.go and change the window configuration in the main() method:

func main() {
	config := Forge.DefaultDesktopConfig()
	config.Title = "Handling Input in Forge"
	config.Width = 800
	config.Height = 500
	if err := Forge.RunSafe(&Game{}, config); err != nil {
		panic(err)
	}
}

We’ll also use a FitViewport with 8×5 world units for rendering and a regular sprite batch for drawing.

Let’s create our assets folder inside the project root and download the two sprites.

You can download the assets from here (Credits are listed below!)


Defining our Player Struct ⛹️‍♂️

We’ll create a new file called player.go and define our player logic there.

Each player will have:

  • a position and velocity
  • a speed value
  • a texture
  • key bindings for movement

We’ll also create three methods:

  • input(driver) for key input (polling)
  • logic(delta) to update the position
  • draw(batch) to draw the sprite

Here’s what the beginning of the file looks like:

type Player struct {
	position mgl32.Vec2
	velocity mgl32.Vec2
	speed    float32
	texture  *Graphics.Texture
	keyUp, keyDown, keyLeft, keyRight Input.Key
}

Polling Input Each Frame ⌨️

Inside our input() method, we’ll reset the velocity and check each direction key:

func (player *Player) input(driver *Forge.Driver) {
	player.velocity[0] = 0
	player.velocity[1] = 0

	if driver.Input.IsKeyPressed(player.keyUp) {
		player.velocity[1]++
	}
	if driver.Input.IsKeyPressed(player.keyDown) {
		player.velocity[1]--
	}
	if driver.Input.IsKeyPressed(player.keyLeft) {
		player.velocity[0]--
	}
	if driver.Input.IsKeyPressed(player.keyRight) {
		player.velocity[0]++
	}

	player.velocity.Normalize()
}

This method is called every frame, and we’ll later multiply velocity by speed * delta in logic().


Creating Our Game Scene 🧩

Now let’s create and open our game file: game.go

We’ll define our implementation of the game there (instead of Application in main.go), so go ahead and clean your main.go file and move the Application struct to the game.go file, and rename it to Game. It should look like this:

package main

import "github.com/ForgeLeaf/Forge"

type Game struct {}

func (game *Game) Create(driver *Forge.Driver) {
	
}

func (game *Game) Render(driver *Forge.Driver, delta float32) {
	
}

func (game *Game) Resize(driver *Forge.Driver, width, height float32) {
	
}

func (game *Game) Destroy(driver *Forge.Driver) {
	
}

We’ll first define our game objects in the struct:

  • batch should be our main sprite batch for rendering.
  • viewport should be our main viewport of the world.
  • goblin represents our first player.
  • werewolf represents our second player.

I’ll save you the headache and tell you that it should like this:

type Game struct {
	batch    *Graphics.Batch
	viewport *Viewports.FitViewport

	goblin   *Player
	werewolf *Player
}

Don’t forget to import the packages github.com/ForgeLeaf/Forge/Graphics and github.com/ForgeLeaf/Forge/Graphics/Viewports and running go mod tidy to fetch them.

So, back to coding, we’ll create our batch, viewport, and players in the Create() method:

game.batch = Graphics.NewBatch(driver)
game.viewport = Viewports.NewFitViewport(8, 5, driver.Width, driver.Height)

Then we load both textures and create our players:

goblinTexture, _ := Graphics.NewTexture("assets/Goblin.png")
game.goblin = NewPlayer(1, 1, goblinTexture, Input.KeyW, Input.KeyS, Input.KeyA, Input.KeyD)

werewolfTexture, _ := Graphics.NewTexture("assets/Werewolf.png")
game.werewolf = NewPlayer(5, 1, werewolfTexture, Input.KeyUp, Input.KeyDown, Input.KeyLeft, Input.KeyRight)

The code won’t compile just yet! That’s because we haven’t created the NewPlayer method 🙃


Implementing the Player ⛹️‍♂️

Let’s go back to our player.go file, we’ve already implemented our input(driver) method, now, our logic method isn’t that hard to implement either:

func (player *Player) logic(delta float32) {
	player.position[0] += delta * player.speed * player.velocity[0]
	player.position[1] += delta * player.speed * player.velocity[1]
}

To break it down: We add the value of velocity * speed * delta to our current position for both the x-Axis and the y-Axis.

To see the player, we’ll create and implement a draw(batch) method:

func (player *Player) draw(batch *Graphics.Batch) {
	batch.Draw(player.texture, player.position[0], player.position[1], 2, 2)
}

Basically, we just tell the batch to draw the player texture at the current player position and we set the width and height of the rendered rectangle to 2×2 world-units.

Now all that’s left is to implement our constructor, the NewPlayer method, which takes in:

  • x and y as initial coordinates/position.
  • texture for the player texture
  • keyUp, keyDown, keyLeft, and keyRight as mappings for movement input keys.

We want all players to have the same speed, so we’ll skip it here for now.

Your method should look like this:

func NewPlayer(x, y float32, texture *Graphics.Texture, keyUp, keyDown, keyLeft, keyRight Input.Key) *Player {
	player := &Player{}
	player.position = mgl32.Vec2{x, y}
	player.velocity = mgl32.Vec2{}
	player.speed = 2
	player.texture = texture
	player.keyUp = keyUp
	player.keyDown = keyDown
	player.keyLeft = keyLeft
	player.keyRight = keyRight
	return player
}

Before saving the file, add the following import statement and run go mod tidy to fetch the required dependencies:

import (
	"github.com/ForgeLeaf/Forge"
	"github.com/ForgeLeaf/Forge/Graphics"
	"github.com/ForgeLeaf/Forge/Input"
	"github.com/go-gl/mathgl/mgl32"
)

Implementing our Game 🏄‍♀️

Now all that’s left is to implement the our game loop, handle resizing and destroy events, and fine-tuning our final game!

Let’s start with the resizing, we basically just need to update our viewport:

func (game *Game) Resize(driver *Forge.Driver, width, height float32) {
	game.viewport.Update(width, height, true)
}

That’s done! Now, to our Destroy() method, we need to dispose the player textures and our main batch, which can easily be achievied using this snippet:

func (game *Game) Destroy(driver *Forge.Driver) {
	game.batch.Dispose()
	game.goblin.texture.Dispose()
	game.werewolf.texture.Dispose()
}

And that’s done too! Now all that’s left is to implement our Render() method. We’ll implement it like a real production-ready game does it. A render-cycle should, in the same order:

  • Handle the input
  • Update the logic
  • Draw the entities

And we’ll apply that exact same principal here. We’ve conveniently created the input, logic, and draw methods in our player.go, so it’s basically just a matter of connecting it to our game loop.

We first handle the input:

game.goblin.input(driver)
game.werewolf.input(driver)

Then, for our logic, we’ll first apply our viewport and then call our players’ logic method:

game.viewport.Apply(true)
game.goblin.logic(delta)
game.werewolf.logic(delta)

And for the final part, we’ll clear our screen, set the batch projection, begin the batch, draw the players, and end the batch:

gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
game.batch.SetProjection(game.viewport.GetCamera().Matrix)
game.batch.Begin()
game.goblin.draw(game.batch)
game.werewolf.draw(game.batch)
game.batch.End()

Finally, now, if you launch the game, it should look similar to this:

Cool right? We can already see our player! Let’s give the background a nice blue-ish color like our beautiful sky:

gl.ClearColor(0.52, 0.72, 0.83, 1)

And it should already look somewhat better:

But wait, what are these black boxes there? Let’s fix it!


Fixing the Black Boxes ⬛

In video game development, you’ll often come across drawing optimization techniques and you’ll hear the word “Blending” alot.

What is blending? It’s an advanced topic that we’ll keep for later but if you’re excited enough to read about it today, here’s a nice article I’d recommend.

Blending is the way of telling the renderer how to render stacked objects, for example: Should it have transparency? Or just ignore it? Should it do something different like clip the objects or complete remove the pixel or whatever.

There are many different blending configurations. For now, we’ll basically just enable and use the simplest blending configuration by adding this snippet of code at the end of our Create() method:

gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

And don’t forget to import the right go-gl package and to run go mod tidy to fetch it:

import "github.com/go-gl/gl/v3.3-core/gl"

Should already look much better:

But the sprites look blurry, why? Let’s fix that too!


Fixing the Blurry Textures

The GPU uses algorithms to scale textures and most game libraries and frameworks default to what’s called bilinear-filtering, a texture-filter that linearly interpolates between the pixel color values to find the most suitable color for a scaled texture.

So, if it’s the most suitable color, why does it look blurry you may ask? It’s because our assets are pixel-art and pixel-art prefers to look sharp in some way. Drawing pixel-art means you’re working with a small canvas size and scaling it to the 10x will result it textures looking too blurry.

The solution? It’s to use a different commonly used texture-filter called nearest-filtering, which works a bit differently and produced pixelated instead of blurred outputs.

Here’s a nice comparison I got from the LearnOpenGL forum:

Credits: https://learnopengl.com/Getting-started/Textures

So, when create our player textures, we use the provided SetFilter(min, mag) method to change the filter of our texture, for the goblin it’s:

goblinTexture.SetFilter(gl.NEAREST, gl.NEAREST)

and for our werewolf it’s:

werewolfTexture.SetFilter(gl.NEAREST, gl.NEAREST)

Run the game and it should look much, much better:


Adding an ESC-Callback 🚦

Lastly, to say that we used a key callback in Forge, we’ll tell our driver to stop the game whenever we press the escape-key (ESC), so go ahead to the end of your Create() method and add this snippet of code:

driver.Input.SetKeyCallback(func(key Input.Key, action Input.Action, modifiers Input.ModifierKey) {
	if key == Input.KeyEscape && action == Input.ActionPress {
		driver.Stop()
	}
})

And that’s it! The application should quit whenever we press the escape-key on our keyboard 🎉🎉🎉

Let’s play a bit with our game, the final code should produce something similar to the showcase below:


The Final Code

main.go

package main

import (
	"runtime"

	"github.com/ForgeLeaf/Forge"
)

func init() {
	runtime.LockOSThread()
}

func main() {
	config := Forge.DefaultDesktopConfig()
	config.Title = "Handling Input in Forge"
	config.Width = 800
	config.Height = 500
	if err := Forge.RunSafe(&Game{}, config); err != nil {
		panic(err)
	}
}

player.go

package main

import (
	"github.com/ForgeLeaf/Forge"
	"github.com/ForgeLeaf/Forge/Graphics"
	"github.com/ForgeLeaf/Forge/Input"
	"github.com/go-gl/mathgl/mgl32"
)

type Player struct {
	position mgl32.Vec2
	velocity mgl32.Vec2
	speed    float32

	texture *Graphics.Texture

	keyUp    Input.Key
	keyDown  Input.Key
	keyLeft  Input.Key
	keyRight Input.Key
}

func NewPlayer(x, y float32, texture *Graphics.Texture, keyUp, keyDown, keyLeft, keyRight Input.Key) *Player {
	player := &Player{}
	player.position = mgl32.Vec2{x, y}
	player.velocity = mgl32.Vec2{}
	player.speed = 2
	player.texture = texture
	player.keyUp = keyUp
	player.keyDown = keyDown
	player.keyLeft = keyLeft
	player.keyRight = keyRight
	return player
}

func (player *Player) input(driver *Forge.Driver) {
	player.velocity[0] = 0
	player.velocity[1] = 0

	// Vertical
	if driver.Input.IsKeyPressed(player.keyUp) {
		player.velocity[1]++
	}
	if driver.Input.IsKeyPressed(player.keyDown) {
		player.velocity[1]--
	}

	// Horizontal
	if driver.Input.IsKeyPressed(player.keyLeft) {
		player.velocity[0]--
	}
	if driver.Input.IsKeyPressed(player.keyRight) {
		player.velocity[0]++
	}

	player.velocity.Normalize()
}

func (player *Player) logic(delta float32) {
	player.position[0] += delta * player.speed * player.velocity[0]
	player.position[1] += delta * player.speed * player.velocity[1]
}

func (player *Player) draw(batch *Graphics.Batch) {
	batch.Draw(player.texture, player.position[0], player.position[1], 2, 2)
}

game.go

package main

import (
	_ "image/png"

	"github.com/ForgeLeaf/Forge"
	"github.com/ForgeLeaf/Forge/Graphics"
	"github.com/ForgeLeaf/Forge/Graphics/Viewports"
	"github.com/ForgeLeaf/Forge/Input"
	"github.com/go-gl/gl/v3.3-core/gl"
)

type Game struct {
	batch    *Graphics.Batch
	viewport *Viewports.FitViewport

	goblin   *Player
	werewolf *Player
}

func (game *Game) Create(driver *Forge.Driver) {
	game.batch = Graphics.NewBatch(driver)
	game.viewport = Viewports.NewFitViewport(8, 5, driver.Width, driver.Height)

	goblinTexture, _ := Graphics.NewTexture("assets/Goblin.png")
	goblinTexture.SetFilter(gl.NEAREST, gl.NEAREST)
	game.goblin = NewPlayer(1, 1, goblinTexture, Input.KeyW, Input.KeyS, Input.KeyA, Input.KeyD)

	werewolfTexture, _ := Graphics.NewTexture("assets/Werewolf.png")
	werewolfTexture.SetFilter(gl.NEAREST, gl.NEAREST)
	game.werewolf = NewPlayer(5, 1, werewolfTexture, Input.KeyUp, Input.KeyDown, Input.KeyLeft, Input.KeyRight)

	gl.Enable(gl.BLEND)
	gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

	driver.Input.SetKeyCallback(func(key Input.Key, action Input.Action, modifiers Input.ModifierKey) {
		if key == Input.KeyEscape && action == Input.ActionPress {
			driver.Stop()
		}
	})
}

func (game *Game) Render(driver *Forge.Driver, delta float32) {
	// input
	game.goblin.input(driver)
	game.werewolf.input(driver)

	// logic
	game.viewport.Apply(true)
	game.goblin.logic(delta)
	game.werewolf.logic(delta)

	// draw
	gl.ClearColor(0.52, 0.72, 0.83, 1)
	gl.Clear(gl.COLOR_BUFFER_BIT)
	game.batch.SetProjection(game.viewport.GetCamera().Matrix)
	game.batch.Begin()
	game.goblin.draw(game.batch)
	game.werewolf.draw(game.batch)
	game.batch.End()
}

func (game *Game) Resize(driver *Forge.Driver, width, height float32) {
	game.viewport.Update(width, height, true)
}

func (game *Game) Destroy(driver *Forge.Driver) {
	game.batch.Dispose()
	game.goblin.texture.Dispose()
	game.werewolf.texture.Dispose()
}

Compare it to your version and tell me if it works! That’s it! You’ve successfully drawn your first texture with Forge. If you’ve faced any issues, please message me using the form below.

You can find the repository for this tutorial here: https://github.com/ForgeLeaf/HandlingInput-Forge


Asset Credits 🔗

🐺Werewolf.png was created by Stephen Challener (Redshrike), William Thompson (William.Thompsonj), & Jordan Irwin (AntumDeluge) and obtained from OpenGameArt.org.

💚Goblin.png was created by Stephen Challener (Redshrike) & William Thompson (William.Thompsonj) and obtained from OpenGameArt.org.


Common Issues

If you’re getting a black screen:

  • Check if you imported the image/png package.
  • Check if you’re rendering within batch.Begin() and batch.End().
  • Check if you’re applying the viewport projection to the batch.

If the code isn’t compiling:

  • Check if you’ve misspelled any imports.
  • Run go mod tidy and rebuild.
  • Clear IDE cache.
  • Check for syntax errors.

If your program is crashing before rendering anything:

  • Check whether you’ve imported the correct go-gl version or not (Correct one is go-gl/gl/v3.3-core/gl).

If you’re facing any other issues, contact me below!

Give us some feedback!

Thanks for reading our article. Could you please consider giving us some constructive, anonymous feedback?

Related articles

Working with ECS in LibGDX 🤩

Bismillah, dear readers!I hope you enjoyed the long weekend and managed to recharge. Today we’ll explore building a simple game using simple‑world‑gdx (my lightweight ECS for LibGDX), together with simple‑batch‑gdx…
Read more

Forge Framework

Forge is our free and open source game framework for the GoLang programming language. It is feature-rich and constantly improved. We designed Forge to be performant and simple for beginner, its power lies within its ecosystem and community. Forge is lightweight in nature and can be easily extended to fit most of your needs. Write you game logic in your way and let the framework handle compatibility.
Community

Connect with our community

ForgeLeaf Logo
Join the ForgeLeaf community — follow updates, contribute, and grow together.

Subscribe to our Newsletter

Stay Updated on the Latest Dev Insights. Join our mailing list to receive new articles, tutorials, and project updates.
ForgeLeaf Logo
Made with ❤️ in 🇩🇪
Follow us on social media
Subscribe to our Newsletter
© 2025 ForgeLeaf Studio