termloop

repository·master·Indexed 23 days ago

https://github.com/joelotter/termloop

A pure Go game engine for building terminal-based games on top of Termbox. It provides a render loop, entity management, collision detection via Physical and DynamicPhysical interfaces, and input handling for keyboard and mouse. Features include a level map parser for JSON, ASCII art entity generation, a 'pixel mode' for increased screen height, and optional audio playback via the termloop/extra package.

Tokens
2K
Snippets
11
Records
16
Agent score
31%

What's inside termloop

  1. Overview of Termloop features

    master

    Termloop is a pure Go game engine for the terminal built on top of Termbox. It provides a render loop designed to make terminal game development easy.

    Key features include:

    • Input Handling: Support for keyboard and mouse input.
    • Game Mechanics: Collision detection and level offsets to simulate 'camera' movement.
    • Rendering: Render timers and an optional 'pixel mode' (which draws two 'pixels' to a single terminal character to double screen height, though text rendering is disabled in this mode).
    • Built-in Entities: Framerate counters, Rectangles, and Text.
    • Data Loading: Support for loading entities from ASCII art, color maps from images, and level maps from JSON.
    • Portability: Being pure Go, it allows for easy portability and built-in cross-compilation.
  2. Implement collision detection with Physical and DynamicPhysical

    master

    Termloop provides two interfaces for handling collisions:

    1. Physical: Represents an object that can be collided with. It must implement Position() (int, int) and Size() (int, int). Use this for static objects like walls or lakes.
    2. DynamicPhysical: Represents an object that can actively process its own collisions. It implements Physical and adds the Collide(Physical) method. Use this for moving objects like players that need to react to hitting something.

    Best Practice: For performance, use Physical for as many objects as possible and reserve DynamicPhysical only for objects that require active logic (like the player).

  3. Use Termloop extras for audio playback

    master

    The termloop/extra package provides functionality that requires external dependencies, meaning using them may prevent your binary from being fully portable.

    To use the audio playback features, you must have the following requirements installed on your system:

    • PortAudio
    • libsndfile
  4. How Levels and Entities work together

    master

    Termloop uses a hierarchy of Screen, Level, and Entity to manage the game world:

    1. Screen: The top-level drawing surface. You can add entities directly to the screen for simple apps (like a HUD), but for camera scrolling or collision detection, you should use a Level.
    2. Level: A container for entities. You can set a level on the screen using game.Screen().SetLevel(level). A BaseLevel can be initialized with a tl.Cell to fill the entire level with a specific background color, foreground color, and character.
    3. Entity: Individual objects within a level. You add them using level.AddEntity(entity).

    Use tl.NewBaseLevel(tl.Cell{...}) to create a level with a uniform background, and level.AddEntity(...) to place objects like tl.Rectangle within it.

    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)
  5. Implement the Drawable interface for custom entities

    master

    To create custom game objects, you should use object composition by embedding *tl.Entity into your own struct. To make your struct renderable and interactive, you must implement the Drawable interface by providing two methods:

    • Draw(screen *tl.Screen): Defines how the entity is drawn to the screen. If you override this, ensure you call the underlying entity's Draw method to maintain standard rendering.
    • Tick(event tl.Event): Handles input and logic updates. You can check event.Type == tl.EventKey to process keyboard input using constants like tl.KeyArrowRight.
    type Player struct {
    	*tl.Entity
    }
    
    func (player *Player) Tick(event tl.Event) {
    	if event.Type == tl.EventKey {
    		x, y := player.Position()
    		switch event.Key {
    		case tl.KeyArrowRight:
    			player.SetPosition(x+1, y)
    		// ... other keys
    		}
    	}
    }
  6. Parse levels from JSON using the level map parser

    master

    Termloop includes a level map parser that can read level data from a JSON string. The parser maps JSON object types to entities. You can also define a map of custom parsing functions to dictate how custom objects (like a Player) are instantiated from JSON data.

    Example JSON structure for a level:

    [
        {
            "type": "Rectangle",
            "data": {
                "x": 5,
                "y": 8,
                "width": 20,
                "height": 7,
                "color": 67
            }
        },
        {
            "type": "Text",
            "data": {
                "x": 7,
                "y": 4,
                "text": "Hello!",
                "fg": 70,
                "bg": 53
            }
        },
        {
            "type": "Entity",
            "data": {
                "x": 60,
                "y": 3,
                "text": "images/chessboard.png",
                "fg": "images/chesspieces.png",
                "bg": "images/chessboard.png"
            }
        },
        {
            "type": "Player",
            "data": {
                "x": 0,
                "y": 0,
                "ch": "@",
                "color": 91
            }
        }
    ]
    go run levelmap.go
  7. Create a blank Termloop game

    master

    To initialize a basic Termloop game, create a new game instance using tl.NewGame() and call .Start() to begin the game loop. This provides a blank terminal screen.

    package main
    
    import tl "github.com/JoelOtter/termloop"
    
    func main() {
    	game := tl.NewGame()
    	game.Start()
    }
  8. Implement camera scrolling using Level offsets

    master

    Termloop does not have a dedicated camera object. Instead, 'scrolling' is achieved by setting an offset on the Level. To keep a player centered, you can override the player's Draw method to calculate the required offset based on the screen size and the player's position, then call level.SetOffset(x, y).

    func (player *Player) Draw(screen *tl.Screen) {
    	screenWidth, screenHeight := screen.Size()
    	x, y := player.Position()
    	player.level.SetOffset(screenWidth/2-x, screenHeight/2-y)
    	// Ensure the underlying Entity is still drawn
    	player.Entity.Draw(screen)
    }