Terminal-based game engine for Go, built on top of Termbox

Termloop

Join the chat at https://gitter.im/JoelOtter/termloop GoDoc

Termloop is a pure Go game engine for the terminal, built on top of the excellent Termbox. It provides a simple render loop for building games in the terminal, and is focused on making terminal game development as easy and as fun as possible.

Termloop is still under active development so changes may be breaking. I add any breaking changes to the Changelog - hopefully at this stage there shouldn't be too many. Pull requests and issues are very welcome, and do feel free to ask any questions you might have on the Gitter. I hope you enjoy using Termloop; I've had a blast making it.

Installing

Install and update with go get -u github.com/JoelOtter/termloop

Features

  • Keyboard and mouse input
  • Collision detection
  • Render timers
  • Level offsets to simulate 'camera' movement
  • Debug logging
  • Built-in entity types such as:
  • Framerate counters
  • Rectangles
  • Text
  • Loading entities from ASCII art
  • Loading colour maps from images
  • Loading level maps from JSON
  • Optional 'pixel mode' - draw two 'pixels' to a terminal character, doubling screen height at the expense of being able to render text.
  • Pure Go - easy portability of compiled games, and cross-compilation built right in.

To see what's on the roadmap, have a look at the issue tracker.

termloop/extra

The Termloop extras are a collection of types and functions, the use of which will not result in a fully portable binary - that is, they have some external dependencies. However, if you're willing to require these dependencies in your project, they should integrate quite nicely with the rest of Termloop. Some of the included examples use these extras.

  • Audio playback
  • audio.go
  • Requirements: PortAudio and libsndfile

Cool stuff built with Termloop

Feel free to add yours with a pull request!

Tutorial

More full documentation will be added to the Wiki soon. In the meantime, check out this tutorial, the GoDoc, or the included examples. If you get stuck during this tutorial, worry not, the full source is here.

Creating a blank Termloop game is as simple as:

package main

import tl "github.com/JoelOtter/termloop"

func main() {
	game := tl.NewGame()
	game.Start()
}

We can press Ctrl+C to exit. It's just a blank screen - let's make it a little more interesting.

Let's make a green background, because grass is really nice to run around on. We create a new level like so:

level := tl.NewBaseLevel(tl.Cell{
	Bg: tl.ColorGreen,
	Fg: tl.ColorBlack,
	Ch: 'v',
})

Cell is a struct that represents one cell on the terminal. We can set its background and foreground colours, and the character that is displayed. Creating a BaseLevel in this way will fill the level with this Cell.

Let's make a nice pretty lake, too. We'll use a Rectangle for this. We'll put the lake at position (10, 10), with width 50 and height 20. All measurements are in terminal characters! The last argument is the colour of the Rectangle.

level.AddEntity(tl.NewRectangle(10, 10, 50, 20, tl.ColorBlue))

We don't need to use a Level - we can add entities directly to the Screen! This is great for building a HUD, or a very simple app. However, if we want camera scrolling or collision detection, we're going to need to use a Level.

Putting together what we have so far:

package main

import tl "github.com/JoelOtter/termloop"

func main() {
	game := tl.NewGame()
	level := tl.NewBaseLevel(tl.Cell{
		Bg: tl.ColorGreen,
		Fg: tl.ColorBlack,
		Ch: 'v',
	})
	level.AddEntity(tl.NewRectangle(10, 10, 50, 20, tl.ColorBlue))
	game.Screen().SetLevel(level)
	game.Start()
}

When we run it with go run tutorial.go, it looks like this:

Pretty! Ish. OK, let's create a character that can walk around the environment. We're going to use object composition here - we'll create a new struct type, which extends an Entity.

To have Termloop draw our new type, we need to implement the Drawable interface, which means we need two methods: Draw() and Tick(). The Draw method defines how our type is drawn to the Screen (Termloop's internal drawing surface), and the Tick method defines how we handle input.

We don't need to do anything special for Draw, and it's already handled by Entity, so we just need a Tick:

type Player struct {
	*tl.Entity
}

func (player *Player) Tick(event tl.Event) {
	if event.Type == tl.EventKey { // Is it a keyboard event?
		x, y := player.Position()
		switch event.Key { // If so, switch on the pressed key.
		case tl.KeyArrowRight:
			player.SetPosition(x+1, y)
		case tl.KeyArrowLeft:
			player.SetPosition(x-1, y)
		case tl.KeyArrowUp:
			player.SetPosition(x, y-1)
		case tl.KeyArrowDown:
			player.SetPosition(x, y+1)
		}
	}
}

Now that we've built our Player type, let's add one to the level. I'm going to use the character '옷', because I think it looks a bit like a stick man.

player := Player{tl.NewEntity(1, 1, 1, 1)}
// Set the character at position (0, 0) on the entity.
player.SetCell(0, 0, &tl.Cell{Fg: tl.ColorRed, Ch: '옷'})
level.AddEntity(&player)

Running the game again, we see that we can now move around the map using the arrow keys. Neato! However, we can stroll across the lake just as easily as the grass. Our character isn't the Messiah, he's a very naughty boy, so let's add some collisions.

In Termloop, we have two interfaces that are used for collisions. Here they are.

// Physical represents something that can collide with another
// Physical, but cannot process its own collisions.
// Optional addition to Drawable.
type Physical interface {
	Position() (int, int) // Return position, x and y
	Size() (int, int)     // Return width and height
}

// DynamicPhysical represents something that can process its own collisions.
// Implementing this is an optional addition to Drawable.
type DynamicPhysical interface {
	Position() (int, int) // Return position, x and y
	Size() (int, int)     // Return width and height
	Collide(Physical)     // Handle collisions with another Physical
}

It's pretty simple - if we want our object to be 'solid', then we implement Physical. If we want a solid object that actually does some processing on its own collisions, we implement DynamicPhysical! Essentially this just involves adding one more method to your type.

Note that, for performance reasons, you should try and have as few DynamicPhysicals as possible - for example, our Player will be one, but the lake need only be a Physical.

The Rectangle type already implements Physical, so we don't actually need to do anything. As well, Player already implements DynamicPhysical because of the embedded Entity. However, we want custom behaviour for Collide, so let's implement that method. For that, we'll have to modify our struct and Tick method, to keep track of the Player's previous position so we can move it back there if it collides with something.

type Player struct {
	*tl.Entity
	prevX  int
	prevY  int
}

func (player *Player) Tick(event tl.Event) {
	if event.Type == tl.EventKey { // Is it a keyboard event?
		player.prevX, player.prevY = player.Position()
		switch event.Key { // If so, switch on the pressed key.
		case tl.KeyArrowRight:
			player.SetPosition(player.prevX+1, player.prevY)
		case tl.KeyArrowLeft:
			player.SetPosition(player.prevX-1, player.prevY)
		case tl.KeyArrowUp:
			player.SetPosition(player.prevX, player.prevY-1)
		case tl.KeyArrowDown:
			player.SetPosition(player.prevX, player.prevY+1)
		}
	}
}

func (player *Player) Collide(collision tl.Physical) {
	// Check if it's a Rectangle we're colliding with
	if _, ok := collision.(*tl.Rectangle); ok {
		player.SetPosition(player.prevX, player.prevY)
	}
}

Not too much extra code! We can now see that the Player can't walk out into the lake. If you see the Player overlap the lake slightly on one side, that's likely because the 'stick man' character we used isn't quite standard width.

We've now got something that looks a bit like a very simple exploration game. There's one more thing to add - let's have the camera scroll to keep the Player in the centre of the screen!

There isn't really a 'camera' in Termloop, like you might find in another graphics library. Instead, we set an offset, and the Screen draws our level appropriately. In our case it's really simple - all we need is for the Player to have a pointer to the Level, so we can make calls on it. Then we simply modify our Draw method, like so:

type Player struct {
	*tl.Entity
	prevX  int
	prevY  int
	level  *tl.BaseLevel
}

func (player *Player) Draw(screen *tl.Screen) {
	screenWidth, screenHeight := screen.Size()
	x, y := player.Position()
	player.level.SetOffset(screenWidth/2-x, screenHeight/2-y)
  // We need to make sure and call Draw on the underlying Entity.
	player.Entity.Draw(screen)
}


// in func main
player := Player{
	Entity:   tl.NewEntity(1, 1, 1, 1),
	level: level,
}

That's all it takes. We should now see the camera moving. Of course, due to the static, repeating background, this doesn't look terribly convincing - it kind of looks like the player is standing still and everything else is moving! We could remedy this by, for example, only updating the offset when the player is closer to the edge of the screen. I'll leave it up to you as a challenge.

We've now reached the end of our tutorial - I hope it's been useful! If you'd like to learn a little more about Termloop, more comprehensive documentation is coming on the Wiki. In the meantime, you can check out the GoDoc, or the included examples. I'll be hanging out on the Gitter too, if you have any questions. Have fun, and please do show me if you make something cool!

Owner
Joel Auterson
Please ignore the fact that most of my top repos are JavaScript, I am trying to be a better person now
Joel Auterson
Comments
  • Example throwing error : Image.go

    Example throwing error : Image.go

    panic: runtime error: index out of range
    
    goroutine 1 [running]:
    github.com/nsf/termbox-go.cell_to_char_info(0x59000000000000, 0x0)
            C:/Users/#/Documents/GoProjects/src/github.com/nsf/termbox-go/termbox_windows.go:526 +0x18b
    github.com/nsf/termbox-go.append_diff_line(0xe, 0x78)
            C:/Users/#/Documents/GoProjects/src/github.com/nsf/termbox-go/termbox_windows.go:466 +0xe0
    github.com/nsf/termbox-go.prepare_diff_messages()
            C:/Users/#/Documents/GoProjects/src/github.com/nsf/termbox-go/termbox_windows.go:510 +0x265
    github.com/nsf/termbox-go.Flush(0x0, 0x0)
            C:/Users/#/Documents/GoProjects/src/github.com/nsf/termbox-go/api_windows.go:109 +0x42
    github.com/JoelOtter/termloop.(*Screen).Draw(0xc082010090)
            C:/Users/#/Documents/GoProjects/src/github.com/JoelOtter/termloop/screen.go:65 +0x32f
    github.com/JoelOtter/termloop.(*Game).Start(0xc08204e4b0)
            C:/Users/#/Documents/GoProjects/src/github.com/JoelOtter/termloop/game.go:115 +0x51f
    main.main()
            C:/Users/#/Documents/GoProjects/src/Delilah/main.go:54 +0x220
    
    goroutine 13 [chan send]:
    github.com/JoelOtter/termloop.poll(0xc082006740)
            C:/Users/#/Documents/GoProjects/src/github.com/JoelOtter/termloop/input.go:33 +0x9a
    created by github.com/JoelOtter/termloop.(*input).start
            C:/Users/#/Documents/GoProjects/src/github.com/JoelOtter/termloop/input.go:19 +0x3c
    

    Above Error encountered when running the image.go example. Used random png, jpeg files. Nothing worked. Thank you :)

  • Termloop abruptly stops

    Termloop abruptly stops

    Sometimes Termloop will simply stop, outputting no useful information, even if debugging is turned on.

    If you've noticed this happening, please post your configuration as a comment. Here's mine:

    OS: Mac OS X 10.10.5 (Yosemite) Go version: 1.5 Terminal: iTerm2 + tmux

  • How to Have Fixed Position Text

    How to Have Fixed Position Text

    I'm trying to create an in-game hud using a Text object that's position is always fixed to the top-left corner of the screen.

    I can get the text to the correct position sorta but it will not be perfectly fixed. It moves to the desired position when the whole screen updates next but it creates this laggy looking effect where part of the grid coordinates gets cut off or appear off screen for a short time.

    Here's what I got in code so far (its chopped out but here is the important bits):

    type GridCoordinates struct {
    	*tl.Text
    }
    
    // positions the coords Text object in top-left.
    func (m *GridCoordinates) Tick(ev tl.Event) {
    	// plr is the player object
    	mx, my := m.Position()
    	x, y := plr.Position()
    	sx, sy := game.Screen().Size()
    	finx := x - sx/2
    	finy := y - sy/2
    	if finx != mx || finy != my {
    		crd := "x" + strconv.Itoa(x) + ",y" + strconv.Itoa(y)
    		m.SetPosition(x-sx/2, y-sy/2)
    		m.SetText(crd)
    	}
    }
    
    GridText := tl.NewText(0, 0, "x0,y0", tl.ColorWhite, tl.ColorBlue)
    coords = GridCoordinates{GridText}
    level.AddEntity(&coords)
    

    This is the visual result:

    image

    How could I get some Text to always stay fixed in the upper left hand corner?

  • Add Key constant values MouseRelease, MouseWheelUp, MouseWheelDown

    Add Key constant values MouseRelease, MouseWheelUp, MouseWheelDown

    Fixes #25

    This just defines the missing mouse constants from termbox-go. There doesn't seem to be anything more needed to handle these mouse events. But let me know if I'm missing something and I will update the patch.

  • Not possible to get screen width and height before game.Start() is called

    Not possible to get screen width and height before game.Start() is called

    I want to create monsters randomly on the field. But game.Screen().Size() has zeros as width and height and I cannot call rand with width and height as parameters. They are get defined only after termbox.Init() and this is done in the Start() method, so I need to wait till game is started.

    Is it made on purpose? Can we init term at the moment of game creation or split Start() method onto Init() and Run() ?

  • Two small changes

    Two small changes

    1. In the comments of #23 @leavengood picked up on the non-necessary break statements inside Go switch blocks. I had noticed this a while back and went ahead and removed them from the repository (hopefully this isn't too presumptuous).
    2. The old link for the portaudio Go bindings package died with Google Code, and it now redirects to a Github page, so the relevant import statement in audio.go needs to be changed.
  • Key events not working

    Key events not working

    Having a weird issue. I'm trying the examples on Linux. With collision.go, key events work. Esc exits, arrow keys move. With movingtext.go however, I cannot exit with Esc, and arrow keys don't work. I'm also going through the tutorial, and there Esc doesn't exit either. It's as if the Entity's Tick never gets called.

  • can not update entity position safely outside Tick()

    can not update entity position safely outside Tick()

    This is the current game loop for BaseLevel:

    1. Tick
    2. Check Collisions
    3. Draw
    

    However, Tick() is only called if there is an event. This means I can not update the entity position based on frame time. For example, I want to move an entity forward every X frames. This would currently only be possible in Draw() which is called after collision checks, so it is unsafe. Moving an entity inside Draw() allows it to be inside another entity, although collision checks should prevent that.

    Since BaseLevel has a lot of features I would like to use, I don't want to write my own. I think this should be fixed and BaseLevel should call Tick on all its entities on every frame and let the entity decide if it wants to react on keyboard input or just the frame being updated.

    Same goes for Screen.Tick()

    Is there any specific reason you drop all EventNone events?

  • not escaping

    not escaping

    for me, go1.8, this doesn't stop when hitting esc key.

    package main
    import "github.com/JoelOtter/termloop"
    func main() {
    	g := termloop.NewGame()
    	m:=termloop.NewText(0, 0, "<=0=>", termloop.ColorWhite, termloop.ColorBlue)
    	g.Screen().AddEntity(m)
    	g.Start()
    }
    
  • Drawing a Rectangle after game.start()?

    Drawing a Rectangle after game.start()?

    Hello,

    I want to draw a border around the screen by using Rectangles. Depending on the screen size the rectangles have to be placed. Because game.Screen().Size() returns (0,0) if it's called before game.start(), I have to add the Rectangles afterwards.

    But the rectangles don't show up after I add them with AddEntity() to the level. I also tried to call the Draw() method but it doesn't show any effect.

    What is the correct way to draw new entities after game.Start() was called?

    my code:

    package main

    
    import tl "github.com/JoelOtter/termloop"                                                                                  
    import "fmt"                                                                                                               
    
    func main() {                                                                                                              
            game := tl.NewGame()                                                                                               
            game.SetDebugOn(true)                                                                                              
            level := tl.NewBaseLevel(tl.Cell{                                                                                  
                    Bg: tl.ColorBlack,                                                                                         
                    Fg: tl.ColorWhite,                                                                                         
            })                                                                                                                 
            game.Screen().SetLevel(level)                                                                                      
    
            //x, y := game.Screen().Size()                                                                                     
            //fmt.Printf("before start x: %v, y: %v\n", x, y)                                                                  
            game.Start()                                                                                                       
            x, y = game.Screen().Size()                                                                                        
            //fmt.Printf("after start x: %v, y: %v\n", x, y)                                                                   
    
            r := tl.NewRectangle(x, y, 1, 1, tl.ColorRed)                                                                      
            level.AddEntity(r)                                                                                                 
            //r.Draw(game.Screen())                                                                                            
            //game.Screen().Draw()                                                                                             
    }   
    
  • Update mouse handling

    Update mouse handling

    Termbox has added support for additional mouse events, including mouse wheel and release detection. Support for this should be added to Termloop - however, as these new features are currently not supported on Windows, it might be better to add them to termloop/extras, or simply wait.

  • CJK Text cannot been displayed correctly

    CJK Text cannot been displayed correctly

    when using the Text component, if the text string is CJK, eg "你好啊", it will only show "你啊". As far as I understand, this is because CJK need two Cell to render. if what i understand is right, i'll make a pull request to fix this.

  • Incorrect mouse handling in pixel mode.

    Incorrect mouse handling in pixel mode.

    As far as I understand, mouse event coordinates correspond to the terminal cell positions, and no support for pixel mode (where every terminal cell contains two pixels) was implemented. The problem can be observed buy enabling the pixel mode in the click.go example.

  • Termloop tutorial

    Termloop tutorial

    The tutorial in the README, along with the GoDoc, serve as a reasonable introduction. However, I think it'd be useful to have some more comprehensive documentation written, along with several tutorials on how to use Termloop's perhaps more-obscure features. We could host on GitHub Pages, which would keep it as part of this repo (just uses a separate branch).

AircraftWar - a game powered by Go+ spx game engine
AircraftWar - a game powered by Go+ spx game engine

AircraftWar - a game powered by Go+ spx game engine How to run Download Go+ and build it. See https://github.com/goplus/gop#how-to-build. Download thi

Jan 5, 2022
FlappyCalf - a game powered by Go+ spx game engine
FlappyCalf - a game powered by Go+ spx game engine

FlappyCalf - a game powered by Go+ spx game engine How to run Download Go+ and build it. See https://github.com/goplus/gop#how-to-build. Download this

Nov 6, 2022
FlappyCalf - a game powered by Go+ spx game engine
FlappyCalf - a game powered by Go+ spx game engine

FlappyCalf - a game powered by Go+ spx game engine How to run Download Go+ and build it. See https://github.com/goplus/gop#how-to-build. Download this

Nov 6, 2022
MazePlay - a game powered by Go+ spx game engine
MazePlay - a game powered by Go+ spx game engine

MazePlay - a game powered by Go+ spx game engine How to run Download Go+ and build it. See https://github.com/goplus/gop#how-to-build. Download this g

Dec 16, 2021
A simple game that I created with Ebiten game library as a way to teach myself Go. Enjoy!
A simple game that I created with Ebiten game library as a way to teach myself Go. Enjoy!

galactic-asteroid-belt Overview A simple game that I created with Ebiten game library as a way to teach myself Go. Enjoy! Run To run, you will need Go

Dec 2, 2021
RundQuiz-Game - This is a Go exercise that implements and builds a quiz game from a list of math questions in a CSV file.

Go RundQuiz Game Exercise details This exercise is broken into two parts to help simplify the process of explaining it as well as to make it easier to

Jan 5, 2022
Simple 2D game to teach myself various things about game development and ECS, etc

2d-grass-game I really don't know what to name this game. Its a big grass field, and its in 2d so....2D Grass game This is a simple 2D game to teach m

Jan 17, 2022
Tick Tack Toe 5 game built with golang

Tick Tack Toe 5 This project of simple game Tick Tack Toe modified for 5-in-a-row version was build specifically for a school project at Westbohemian

Nov 14, 2021
Command line Snake Game built with Golang

Commandline Snake Game Commandline Snake Game built with Golang. Took about a whole day to complete. Had breaks inbetween progress made. Assumptions H

Jan 22, 2022
Dice game built with golang

Untuk menjalankan game, harap untuk mendownload apk Go di web https://go.dev/ se

Jan 25, 2022
Engo is an open-source 2D game engine written in Go.

Engo A cross-platform game engine written in Go following an interpretation of the Entity Component System paradigm. Engo is currently compilable for

Dec 26, 2022
Go 3D Game Engine
Go 3D Game Engine

G3N - Go 3D Game Engine G3N (pronounced "gen") is an OpenGL 3D Game Engine written in Go. It can be used to write cross-platform Go applications that

Jan 9, 2023
Scalable Distributed Game Server Engine with Hot Swapping in Golang
Scalable Distributed Game Server Engine with Hot Swapping in Golang

GoWorld Scalable Distributed Game Server Engine with Hot Reload in Golang Features Architecture Introduction Get GoWorld Manage GoWorld Servers Demos

Dec 25, 2022
A pure Go game engine
A pure Go game engine

Oak A pure Go game engine Table of Contents Installation Motivation Features Support Quick Start Implementation and Examples Finished Games Installati

Jan 8, 2023
A 2D ARPG game engine.
A 2D ARPG game engine.

Abyss Engine is an ARPG game engine in the same vein of the 2000's games, and supports playing games similar to Diablo 2. The engine is written in golang and is cross platform. This engine does not ship with game specific files, and will require a game's assets in order to run.

Dec 24, 2022
A small fantasy game engine in WASM using GoLang
A small fantasy game engine in WASM using GoLang

The GoLang Fantasy Engine (GoLF Engine) is a retro game engine. It draws inspiration from fantasy console projects like pico-8, tic-80, and pyxle. Like those projects it is designed to be a retro-feeling game creation/playing tool. Unlike those projects GoLF is more minimal in scope and only provides an API and a small set of tools to help you create your games. Tools like an image editor and code editor are not built in. Despite this minimalism creating games in GoLF is still easy and should still maintain the retro game feel.

Jul 16, 2022
golang powered game engine
golang powered game engine

Gobatch Go powered engine that offers features from low level opengl abstraction to UI framework. I created this to separate lot of logic from game am

Nov 13, 2022
spx - A 2D Game Engine for learning Go+
spx - A 2D Game Engine for learning Go+

spx - A 2D Game Engine for learning Go+ Tutorials How to run spx tutorials? Download Go+ and build it. See https://github.com/goplus/gop#how-to-build.

Dec 1, 2022
Go Game Engine using SDL for fun

nMage nMage is a (hopefully!) high performance 3D Game Engine written in Go being developed live, with recordings posted on YouTube. This project is b

Nov 30, 2022