├── .gitignore ├── LICENSE ├── README.md ├── _examples ├── README.md ├── audio.go ├── chessboard.txt ├── click.go ├── collision.go ├── entityfromfile.go ├── image.go ├── images │ ├── audio.png │ ├── chessboard.png │ ├── chesspieces.png │ ├── click.png │ ├── collision1.png │ ├── collision2.png │ ├── entityfromfile.png │ ├── image.png │ ├── levelmap.png │ ├── maze.png │ ├── movingtext.png │ ├── pyramid.png │ ├── tutorial01.png │ ├── tutorial02.png │ └── tutorial03.png ├── level.json ├── levelmap.go ├── lorry.txt ├── movingtext.go ├── pyramid.go └── tutorial.go ├── entity.go ├── extra └── audio.go ├── game.go ├── image.go ├── input.go ├── level.go ├── map.go ├── rectangle.go ├── screen.go ├── termloop.go ├── text.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Joel Auterson & termloop authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | === Termbox === 22 | Copyright (C) 2012 termbox-go authors 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining a copy 25 | of this software and associated documentation files (the "Software"), to deal 26 | in the Software without restriction, including without limitation the rights 27 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 28 | copies of the Software, and to permit persons to whom the Software is 29 | furnished to do so, subject to the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be included in 32 | all copies or substantial portions of the Software. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 39 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 40 | THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Termloop 2 | 3 | [![Join the chat at https://gitter.im/JoelOtter/termloop](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/JoelOtter/termloop?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GoDoc](https://godoc.org/github.com/JoelOtter/termloop?status.svg)](http://godoc.org/github.com/JoelOtter/termloop) 4 | 5 | ![](_examples/images/maze.png) 6 | 7 | Termloop is a pure Go game engine for the terminal, built on top of the excellent [Termbox](https://github.com/nsf/termbox-go). 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. 8 | 9 | Termloop is still under active development so changes may be breaking. I add any breaking changes to the [Changelog](https://github.com/JoelOtter/termloop/wiki/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. 10 | 11 | ## Installing 12 | Install and update with `go get -u github.com/JoelOtter/termloop` 13 | 14 | ## Features 15 | 16 | - Keyboard and mouse input 17 | - Collision detection 18 | - Render timers 19 | - Level offsets to simulate 'camera' movement 20 | - Debug logging 21 | - Built-in entity types such as: 22 | - Framerate counters 23 | - Rectangles 24 | - Text 25 | - Loading entities from ASCII art 26 | - Loading colour maps from images 27 | - Loading level maps from JSON 28 | - Optional 'pixel mode' - draw two 'pixels' to a terminal character, doubling screen height at the expense of being able to render text. 29 | - Pure Go - easy portability of compiled games, and cross-compilation built right in. 30 | 31 | *To see what's on the roadmap, have a look at the [issue tracker](https://github.com/JoelOtter/termloop/issues).* 32 | 33 | ## termloop/extra 34 | 35 | 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. 36 | 37 | - Audio playback 38 | - audio.go 39 | - Requirements: PortAudio and libsndfile 40 | 41 | ## Cool stuff built with Termloop 42 | 43 | - [Included examples](https://github.com/JoelOtter/termloop/tree/master/_examples) (@JoelOtter) 44 | - [Number Crusher](https://github.com/aquilax/number_crusher) (@aquilax) 45 | - [Go Tapper](https://github.com/swapagarwal/gotapper) (@swapagarwal) 46 | - [Frame Assault](https://github.com/Ariemeth/frame_assault) (@Ariemeth) 47 | - [Minesweeper](https://github.com/ryanbaer/minesweeper-go) (@ryanbaer) 48 | - [Termtank](https://github.com/TerrySolar/termtank) (@TerrySolar) 49 | - [Snake](https://github.com/mattkelly/snake-go) (@mattkelly) 50 | - [Go Man's Sky](https://rawktron.itch.io/go-mans-sky) (@rawktron) 51 | - [conwaygo](https://github.com/buckley-w-david/conwaygo) (@buckley-w-david) 52 | - [Doric (a Columns clone)](https://github.com/svera/doric) (@svera) 53 | - [Terminal-based Snake](https://github.com/tristangoossens/snake-go) (@tristangoossens) 54 | - [Sokoban](https://github.com/tristangoossens/sokoban-go) (@tristangoossens) 55 | - [Gopher Typer](https://github.com/ScottBrooks/gopher_typer) (@scottbrooksca) 56 | - [Tetris](https://bitbucket.org/cam73/tetris) (@cam73) 57 | - [Gorched](https://github.com/zladovan/gorched) (@zladovan) 58 | - [Go Invaders](https://github.com/afagundes/go-invaders) (@afagundes) 59 | 60 | _Feel free to add yours with a pull request!_ 61 | 62 | ## Tutorial 63 | 64 | > More full documentation will be added to the Wiki soon. In the meantime, check out this tutorial, the [GoDoc](http://godoc.org/github.com/JoelOtter/termloop), or the [included examples](https://github.com/JoelOtter/termloop/tree/master/_examples). If you get stuck during this tutorial, worry not, the full source is [here](https://github.com/JoelOtter/termloop/blob/master/_examples/tutorial.go). 65 | 66 | Creating a blank Termloop game is as simple as: 67 | 68 | ```go 69 | package main 70 | 71 | import tl "github.com/JoelOtter/termloop" 72 | 73 | func main() { 74 | game := tl.NewGame() 75 | game.Start() 76 | } 77 | ``` 78 | 79 | We can press Ctrl+C to exit. It's just a blank screen - let's make it a little more interesting. 80 | 81 | Let's make a green background, because grass is really nice to run around on. We create a new level like so: 82 | 83 | ```go 84 | level := tl.NewBaseLevel(tl.Cell{ 85 | Bg: tl.ColorGreen, 86 | Fg: tl.ColorBlack, 87 | Ch: 'v', 88 | }) 89 | ``` 90 | 91 | 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](http://godoc.org/github.com/JoelOtter/termloop#BaseLevel) in this way will fill the level with this Cell. 92 | 93 | Let's make a nice pretty lake, too. We'll use a [Rectangle](http://godoc.org/github.com/JoelOtter/termloop#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. 94 | 95 | ```go 96 | level.AddEntity(tl.NewRectangle(10, 10, 50, 20, tl.ColorBlue)) 97 | ``` 98 | 99 | We don't need to use a Level - we can add entities directly to the [Screen](http://godoc.org/github.com/JoelOtter/termloop#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. 100 | 101 | Putting together what we have so far: 102 | 103 | ```go 104 | package main 105 | 106 | import tl "github.com/JoelOtter/termloop" 107 | 108 | func main() { 109 | game := tl.NewGame() 110 | level := tl.NewBaseLevel(tl.Cell{ 111 | Bg: tl.ColorGreen, 112 | Fg: tl.ColorBlack, 113 | Ch: 'v', 114 | }) 115 | level.AddEntity(tl.NewRectangle(10, 10, 50, 20, tl.ColorBlue)) 116 | game.Screen().SetLevel(level) 117 | game.Start() 118 | } 119 | ``` 120 | 121 | When we run it with `go run tutorial.go`, it looks like this: 122 | 123 | ![](_examples/images/tutorial01.png) 124 | 125 | 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](http://godoc.org/github.com/JoelOtter/termloop#Entity). 126 | 127 | To have Termloop draw our new type, we need to implement the [Drawable](http://godoc.org/github.com/JoelOtter/termloop#Drawable) interface, which means we need two methods: **Draw()** and **Tick()**. The Draw method defines how our type is drawn to the [Screen](http://godoc.org/github.com/JoelOtter/termloop#Screen) (Termloop's internal drawing surface), and the Tick method defines how we handle input. 128 | 129 | We don't need to do anything special for `Draw`, and it's already handled by `Entity`, so we just need a `Tick`: 130 | 131 | ```go 132 | type Player struct { 133 | *tl.Entity 134 | } 135 | 136 | func (player *Player) Tick(event tl.Event) { 137 | if event.Type == tl.EventKey { // Is it a keyboard event? 138 | x, y := player.Position() 139 | switch event.Key { // If so, switch on the pressed key. 140 | case tl.KeyArrowRight: 141 | player.SetPosition(x+1, y) 142 | case tl.KeyArrowLeft: 143 | player.SetPosition(x-1, y) 144 | case tl.KeyArrowUp: 145 | player.SetPosition(x, y-1) 146 | case tl.KeyArrowDown: 147 | player.SetPosition(x, y+1) 148 | } 149 | } 150 | } 151 | ``` 152 | 153 | 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. 154 | 155 | ```go 156 | player := Player{tl.NewEntity(1, 1, 1, 1)} 157 | // Set the character at position (0, 0) on the entity. 158 | player.SetCell(0, 0, &tl.Cell{Fg: tl.ColorRed, Ch: '옷'}) 159 | level.AddEntity(&player) 160 | 161 | ``` 162 | 163 | ![](_examples/images/tutorial02.png) 164 | 165 | 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. 166 | 167 | In Termloop, we have two interfaces that are used for collisions. Here they are. 168 | 169 | ```go 170 | // Physical represents something that can collide with another 171 | // Physical, but cannot process its own collisions. 172 | // Optional addition to Drawable. 173 | type Physical interface { 174 | Position() (int, int) // Return position, x and y 175 | Size() (int, int) // Return width and height 176 | } 177 | 178 | // DynamicPhysical represents something that can process its own collisions. 179 | // Implementing this is an optional addition to Drawable. 180 | type DynamicPhysical interface { 181 | Position() (int, int) // Return position, x and y 182 | Size() (int, int) // Return width and height 183 | Collide(Physical) // Handle collisions with another Physical 184 | } 185 | ``` 186 | 187 | 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. 188 | 189 | 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. 190 | 191 | 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. 192 | 193 | 194 | 195 | ```go 196 | type Player struct { 197 | *tl.Entity 198 | prevX int 199 | prevY int 200 | } 201 | 202 | func (player *Player) Tick(event tl.Event) { 203 | if event.Type == tl.EventKey { // Is it a keyboard event? 204 | player.prevX, player.prevY = player.Position() 205 | switch event.Key { // If so, switch on the pressed key. 206 | case tl.KeyArrowRight: 207 | player.SetPosition(player.prevX+1, player.prevY) 208 | case tl.KeyArrowLeft: 209 | player.SetPosition(player.prevX-1, player.prevY) 210 | case tl.KeyArrowUp: 211 | player.SetPosition(player.prevX, player.prevY-1) 212 | case tl.KeyArrowDown: 213 | player.SetPosition(player.prevX, player.prevY+1) 214 | } 215 | } 216 | } 217 | 218 | func (player *Player) Collide(collision tl.Physical) { 219 | // Check if it's a Rectangle we're colliding with 220 | if _, ok := collision.(*tl.Rectangle); ok { 221 | player.SetPosition(player.prevX, player.prevY) 222 | } 223 | } 224 | 225 | ``` 226 | 227 | 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. 228 | 229 | 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! 230 | 231 | 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: 232 | 233 | ```go 234 | type Player struct { 235 | *tl.Entity 236 | prevX int 237 | prevY int 238 | level *tl.BaseLevel 239 | } 240 | 241 | func (player *Player) Draw(screen *tl.Screen) { 242 | screenWidth, screenHeight := screen.Size() 243 | x, y := player.Position() 244 | player.level.SetOffset(screenWidth/2-x, screenHeight/2-y) 245 | // We need to make sure and call Draw on the underlying Entity. 246 | player.Entity.Draw(screen) 247 | } 248 | 249 | 250 | // in func main 251 | player := Player{ 252 | Entity: tl.NewEntity(1, 1, 1, 1), 253 | level: level, 254 | } 255 | ``` 256 | 257 | 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. 258 | 259 | ![](_examples/images/tutorial03.png) 260 | 261 | 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](http://godoc.org/github.com/JoelOtter/termloop), or the [included examples](https://github.com/JoelOtter/termloop/tree/master/_examples). I'll be hanging out on the [Gitter](https://gitter.im/JoelOtter/termloop) too, if you have any questions. Have fun, and please do show me if you make something cool! 262 | -------------------------------------------------------------------------------- /_examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains several small examples, which will hopefully show how to use some of Termloop's features. 4 | 5 | ## movingtext.go 6 | 7 | This example demonstrates how to use keyboard input, as well as the Text builtin. To run, just do: 8 | 9 | `go run movingtext.go "Some text here"` 10 | 11 | ![](images/movingtext.png) 12 | 13 | ## collision.go 14 | 15 | This example demonstrates how to use Termloop's built in collision checking, as well as simple keyboard input. It also includes an example of an FpsText. The player's rectangle will turn blue and stop when it collides with something. To run, just do: 16 | 17 | `go run collision.go` 18 | 19 | ![](images/collision1.png) 20 | ![](images/collision2.png) 21 | 22 | ## click.go 23 | 24 | This very simple example shows how to use Termloop's mouse click detection. It's simply a grid of rectangles which change colour when you click on them - you can use it as a very primitive sort of MS Paint! You can see in the screenshot my inner child artist has come out. 25 | 26 | To run: 27 | 28 | `go run click.go` 29 | 30 | ![](images/click.png) 31 | 32 | ## Pyramid! 33 | 34 | You've started at the top of a pyramid - how many levels down can you get before you're helplessly lost? 35 | 36 | This is a bit of a bigger example, showcasing Termloop's collision detection, as well as level offsets, which can be used to simulate camera movement. The mazes are all randomly generated using [Prim's algorithm](https://en.wikipedia.org/wiki/Maze_generation_algorithm#Randomized_Prim.27s_algorithm). 37 | This example also gives a demo of how Termloop's debug logging works. 38 | 39 | To run: 40 | 41 | `go run pyramid.go` 42 | 43 | ![](images/pyramid.png) 44 | 45 | ## image.go 46 | 47 | This example shows Termloop's image loading capabilities. Simply run the example with a path to an image as the first parameter, and you'll see a very simple image viewer. It can load PNG, JPEG or GIF images. Here's an example of what it looks like showing [Tempa T](http://www.youtube.com/watch?v=OZ6G7qwjom4). 48 | 49 | This example makes use of Termloop's **pixel mode**. This splits terminal characters into two, giving double the available screen height, while sacrificing the ability to draw text. 50 | 51 | ![](images/image.png) 52 | 53 | ## entityfromfile.go 54 | 55 | This example shows Termloop's ability to generate an Entity from ASCII art provided to it as a string. This example loads the file lorry.txt, and renders its contents to the screen. 56 | 57 | To run: 58 | 59 | `go run entityfromfile.go` 60 | 61 | ![](images/entityfromfile.png) 62 | 63 | ## levelmap.go 64 | 65 | This example demonstrates how to use Termloop's level map parser. This can read an entire level's data from a JSON string, and render it appropriately into the level. For example, the following JSON: 66 | 67 | ```json 68 | [ 69 | { 70 | "type": "Rectangle", 71 | "data": { 72 | "x": 5, 73 | "y": 8, 74 | "width": 20, 75 | "height": 7, 76 | "color": 67 77 | } 78 | }, 79 | { 80 | "type": "Rectangle", 81 | "data": { 82 | "x": 7, 83 | "y": 9, 84 | "width": 25, 85 | "height": 9, 86 | "color": 35 87 | } 88 | }, 89 | { 90 | "type": "Text", 91 | "data": { 92 | "x": 7, 93 | "y": 4, 94 | "text": "Hello! The thing to the right is a chess board.", 95 | "fg": 70, 96 | "bg": 53 97 | } 98 | }, 99 | { 100 | "type": "Entity", 101 | "data": { 102 | "x": 35, 103 | "y": 12, 104 | "text": "lorry.txt", 105 | "fg": "", 106 | "bg": "" 107 | } 108 | }, 109 | { 110 | "type": "Entity", 111 | "data": { 112 | "x": 60, 113 | "y": 3, 114 | "text": "chessboard.txt", 115 | "fg": "images/chesspieces.png", 116 | "bg": "images/chessboard.png" 117 | } 118 | }, 119 | { 120 | "type": "Player", 121 | "data": { 122 | "x": 0, 123 | "y": 0, 124 | "ch": "@", 125 | "color": 91 126 | } 127 | } 128 | ] 129 | 130 | ``` 131 | 132 | Will render like so: 133 | 134 | ![](images/levelmap.png) 135 | 136 | Additionally, a user may define a map of custom parsing functions, which dictate how to read a custom object, just as the Player object in the above example, from JSON and into a real entity. 137 | 138 | To run this example, simply do: 139 | 140 | `go run levelmap.go` 141 | 142 | Please see the example's source code, and the map.go source code, for a more detailed explanation. 143 | 144 | # Termloop extras examples 145 | 146 | These examples use the termloop/extra package, which means that they have some external dependencies and may not run without some additional libraries being installed. 147 | 148 | ## audio.go 149 | 150 | Provides a very simple audio player. Depends on PortAudio and libsndfile being installed. Supported file types are whatever is supported by libsndfile - for example, WAV and OGG are both tested to work. 151 | 152 | To run: 153 | 154 | `go run audio.go [path to audio file]` 155 | 156 | ![](images/audio.png) 157 | -------------------------------------------------------------------------------- /_examples/audio.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // A very simple music player. Takes an audio file as its argument. 4 | 5 | import ( 6 | tl "github.com/JoelOtter/termloop" 7 | tlx "github.com/JoelOtter/termloop/extra" 8 | "os" 9 | ) 10 | 11 | func chk(err error) { 12 | if err != nil { 13 | panic(err) 14 | } 15 | } 16 | 17 | type SoundCtrl struct { 18 | audio *tlx.Audio 19 | text []*tl.Text 20 | track *tlx.Track 21 | } 22 | 23 | func CreateSoundCtrl(filename string, loop bool) *SoundCtrl { 24 | a, err := tlx.InitAudio() 25 | chk(err) 26 | t1 := tl.NewText(1, 1, "Push the right arrow to play", tl.ColorWhite, 0) 27 | t2 := tl.NewText(1, 3, "Push the up arrow to pause", tl.ColorWhite, 0) 28 | t3 := tl.NewText(1, 5, "Push the left arrow to restart", tl.ColorWhite, 0) 29 | t4 := tl.NewText(1, 7, "Push the down arrow to stop", tl.ColorWhite, 0) 30 | text := []*tl.Text{t1, t2, t3, t4} 31 | track, err := a.LoadTrack(filename, loop) 32 | chk(err) 33 | return &SoundCtrl{ 34 | audio: a, 35 | track: track, 36 | text: text, 37 | } 38 | } 39 | 40 | func (sc *SoundCtrl) Draw(s *tl.Screen) { 41 | for _, t := range sc.text { 42 | t.Draw(s) 43 | } 44 | } 45 | 46 | func (sc *SoundCtrl) Tick(ev tl.Event) { 47 | if ev.Type == tl.EventKey { 48 | switch ev.Key { 49 | case tl.KeyArrowRight: 50 | sc.track.Play() 51 | case tl.KeyArrowLeft: 52 | sc.track.Restart() 53 | case tl.KeyArrowUp: 54 | sc.track.Pause() 55 | case tl.KeyArrowDown: 56 | sc.track.Stop() 57 | } 58 | } 59 | } 60 | 61 | func main() { 62 | g := tl.NewGame() 63 | sound := CreateSoundCtrl(os.Args[1], true) 64 | defer sound.audio.Stop() 65 | g.Screen().AddEntity(sound) 66 | g.Start() 67 | } 68 | -------------------------------------------------------------------------------- /_examples/chessboard.txt: -------------------------------------------------------------------------------- 1 | KNBQKBNR 2 | PPPPPPPP 3 | 4 | 5 | 6 | 7 | PPPPPPPP 8 | KNBQKBNR 9 | -------------------------------------------------------------------------------- /_examples/click.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | tl "github.com/JoelOtter/termloop" 6 | ) 7 | 8 | type EventInfo struct { 9 | *tl.Text 10 | } 11 | 12 | func NewEventInfo(x, y int) *EventInfo { 13 | return &EventInfo{tl.NewText(x, y, "Click somewhere", tl.ColorWhite, tl.ColorBlack)} 14 | } 15 | 16 | func (info *EventInfo) Tick(ev tl.Event) { 17 | if ev.Type != tl.EventMouse { 18 | return 19 | } 20 | var name string 21 | switch ev.Key { 22 | case tl.MouseLeft: 23 | name = "Mouse Left" 24 | case tl.MouseMiddle: 25 | name = "Mouse Middle" 26 | case tl.MouseRight: 27 | name = "Mouse Right" 28 | case tl.MouseWheelUp: 29 | name = "Mouse Wheel Up" 30 | case tl.MouseWheelDown: 31 | name = "Mouse Wheel Down" 32 | case tl.MouseRelease: 33 | name = "Mouse Release" 34 | default: 35 | name = fmt.Sprintf("Unknown Key (%#x)", ev.Key) 36 | } 37 | info.SetText(fmt.Sprintf("%s @ [%d, %d]", name, ev.MouseX, ev.MouseY)) 38 | } 39 | 40 | type Clickable struct { 41 | *tl.Rectangle 42 | } 43 | 44 | func NewClickable(x, y, w, h int, col tl.Attr) *Clickable { 45 | return &Clickable{tl.NewRectangle(x, y, w, h, col)} 46 | } 47 | 48 | func (c *Clickable) Tick(ev tl.Event) { 49 | x, y := c.Position() 50 | if ev.Type == tl.EventMouse && ev.MouseX == x && ev.MouseY == y { 51 | if c.Color() == tl.ColorWhite { 52 | c.SetColor(tl.ColorBlack) 53 | } else { 54 | c.SetColor(tl.ColorWhite) 55 | } 56 | } 57 | } 58 | 59 | func main() { 60 | g := tl.NewGame() 61 | g.Screen().SetFps(60) 62 | g.Screen().AddEntity(NewEventInfo(0, 0)) 63 | for i := 0; i < 40; i++ { 64 | for j := 1; j < 20; j++ { 65 | g.Screen().AddEntity(NewClickable(i, j, 1, 1, tl.ColorWhite)) 66 | } 67 | } 68 | 69 | g.Start() 70 | } 71 | -------------------------------------------------------------------------------- /_examples/collision.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import tl "github.com/JoelOtter/termloop" 4 | 5 | type CollRec struct { 6 | *tl.Rectangle 7 | move bool 8 | px int 9 | py int 10 | } 11 | 12 | func NewCollRec(x, y, w, h int, color tl.Attr, move bool) *CollRec { 13 | return &CollRec{ 14 | Rectangle: tl.NewRectangle(x, y, w, h, color), 15 | move: move, 16 | } 17 | } 18 | 19 | func (r *CollRec) Tick(ev tl.Event) { 20 | // Enable arrow key movement 21 | if ev.Type == tl.EventKey && r.move { 22 | r.px, r.py = r.Position() 23 | switch ev.Key { 24 | case tl.KeyArrowRight: 25 | r.SetPosition(r.px+1, r.py) 26 | case tl.KeyArrowLeft: 27 | r.SetPosition(r.px-1, r.py) 28 | case tl.KeyArrowUp: 29 | r.SetPosition(r.px, r.py-1) 30 | case tl.KeyArrowDown: 31 | r.SetPosition(r.px, r.py+1) 32 | } 33 | } 34 | } 35 | 36 | func (r *CollRec) Collide(p tl.Physical) { 37 | // Check if it's a CollRec we're colliding with 38 | if _, ok := p.(*CollRec); ok && r.move { 39 | r.SetColor(tl.ColorBlue) 40 | r.SetPosition(r.px, r.py) 41 | } 42 | } 43 | 44 | func main() { 45 | g := tl.NewGame() 46 | g.Screen().SetFps(60) 47 | l := tl.NewBaseLevel(tl.Cell{ 48 | Bg: tl.ColorWhite, 49 | }) 50 | l.AddEntity(NewCollRec(3, 3, 3, 3, tl.ColorRed, true)) 51 | l.AddEntity(NewCollRec(7, 4, 3, 3, tl.ColorGreen, false)) 52 | g.Screen().SetLevel(l) 53 | g.Screen().AddEntity(tl.NewFpsText(0, 0, tl.ColorRed, tl.ColorDefault, 0.5)) 54 | g.Start() 55 | } 56 | -------------------------------------------------------------------------------- /_examples/entityfromfile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | tl "github.com/JoelOtter/termloop" 5 | "io/ioutil" 6 | ) 7 | 8 | func check(e error) { 9 | if e != nil { 10 | panic(e) 11 | } 12 | } 13 | 14 | func main() { 15 | g := tl.NewGame() 16 | g.Screen().SetFps(60) 17 | dat, err := ioutil.ReadFile("lorry.txt") 18 | check(err) 19 | e := tl.NewEntityFromCanvas(1, 1, tl.CanvasFromString(string(dat))) 20 | g.Screen().AddEntity(e) 21 | g.Start() 22 | } 23 | -------------------------------------------------------------------------------- /_examples/image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // A very simple image viewer, which uses Termloop's pixel mode 4 | 5 | import ( 6 | "fmt" 7 | tl "github.com/JoelOtter/termloop" 8 | "os" 9 | ) 10 | 11 | type Image struct { 12 | e *tl.Entity 13 | } 14 | 15 | func NewImage(c *tl.Canvas) *Image { 16 | i := Image{e: tl.NewEntity(0, 0, len(*c), len((*c)[0]))} 17 | i.e.ApplyCanvas(c) 18 | return &i 19 | } 20 | 21 | func (i *Image) Draw(s *tl.Screen) { i.e.Draw(s) } 22 | 23 | func (i *Image) Tick(ev tl.Event) { 24 | // Enable arrow key movement 25 | if ev.Type == tl.EventKey { 26 | x, y := i.e.Position() 27 | switch ev.Key { 28 | case tl.KeyArrowRight: 29 | x -= 1 30 | case tl.KeyArrowLeft: 31 | x += 1 32 | case tl.KeyArrowUp: 33 | y += 1 34 | case tl.KeyArrowDown: 35 | y -= 1 36 | } 37 | i.e.SetPosition(x, y) 38 | } 39 | } 40 | 41 | func main() { 42 | if len(os.Args) < 2 { 43 | fmt.Println("Please provide a filepath to open") 44 | return 45 | } 46 | 47 | g := tl.NewGame() 48 | g.Screen().SetFps(30) 49 | g.Screen().EnablePixelMode() 50 | c := tl.BackgroundCanvasFromFile(os.Args[1]) 51 | g.Screen().AddEntity(NewImage(c)) 52 | g.Start() 53 | } 54 | -------------------------------------------------------------------------------- /_examples/images/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/audio.png -------------------------------------------------------------------------------- /_examples/images/chessboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/chessboard.png -------------------------------------------------------------------------------- /_examples/images/chesspieces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/chesspieces.png -------------------------------------------------------------------------------- /_examples/images/click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/click.png -------------------------------------------------------------------------------- /_examples/images/collision1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/collision1.png -------------------------------------------------------------------------------- /_examples/images/collision2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/collision2.png -------------------------------------------------------------------------------- /_examples/images/entityfromfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/entityfromfile.png -------------------------------------------------------------------------------- /_examples/images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/image.png -------------------------------------------------------------------------------- /_examples/images/levelmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/levelmap.png -------------------------------------------------------------------------------- /_examples/images/maze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/maze.png -------------------------------------------------------------------------------- /_examples/images/movingtext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/movingtext.png -------------------------------------------------------------------------------- /_examples/images/pyramid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/pyramid.png -------------------------------------------------------------------------------- /_examples/images/tutorial01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/tutorial01.png -------------------------------------------------------------------------------- /_examples/images/tutorial02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/tutorial02.png -------------------------------------------------------------------------------- /_examples/images/tutorial03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoelOtter/termloop/5f7c38744afb0c614d544fe5bc72f7259d03c1e1/_examples/images/tutorial03.png -------------------------------------------------------------------------------- /_examples/level.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Rectangle", 4 | "data": { 5 | "x": 5, 6 | "y": 8, 7 | "width": 20, 8 | "height": 7, 9 | "color": 67 10 | } 11 | }, 12 | { 13 | "type": "Rectangle", 14 | "data": { 15 | "x": 7, 16 | "y": 9, 17 | "width": 25, 18 | "height": 9, 19 | "color": 35 20 | } 21 | }, 22 | { 23 | "type": "Text", 24 | "data": { 25 | "x": 7, 26 | "y": 4, 27 | "text": "Hello! The thing to the right is a chess board.", 28 | "fg": 70, 29 | "bg": 53 30 | } 31 | }, 32 | { 33 | "type": "Entity", 34 | "data": { 35 | "x": 35, 36 | "y": 12, 37 | "text": "lorry.txt", 38 | "fg": "", 39 | "bg": "" 40 | } 41 | }, 42 | { 43 | "type": "Entity", 44 | "data": { 45 | "x": 60, 46 | "y": 3, 47 | "text": "chessboard.txt", 48 | "fg": "images/chesspieces.png", 49 | "bg": "images/chessboard.png" 50 | } 51 | }, 52 | { 53 | "type": "Player", 54 | "data": { 55 | "x": 0, 56 | "y": 0, 57 | "ch": "@", 58 | "color": 91 59 | } 60 | } 61 | ] 62 | -------------------------------------------------------------------------------- /_examples/levelmap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | tl "github.com/JoelOtter/termloop" 5 | "io/ioutil" 6 | ) 7 | 8 | type Player struct { 9 | *tl.Entity 10 | } 11 | 12 | func (p *Player) Tick(ev tl.Event) { 13 | // Enable arrow key movement 14 | if ev.Type == tl.EventKey { 15 | x, y := p.Position() 16 | switch ev.Key { 17 | case tl.KeyArrowRight: 18 | x += 1 19 | case tl.KeyArrowLeft: 20 | x -= 1 21 | case tl.KeyArrowUp: 22 | y -= 1 23 | case tl.KeyArrowDown: 24 | y += 1 25 | } 26 | p.SetPosition(x, y) 27 | } 28 | } 29 | 30 | // Here we define a parse function for reading a Player out of JSON. 31 | func parsePlayer(data map[string]interface{}) tl.Drawable { 32 | e := tl.NewEntity( 33 | int(data["x"].(float64)), 34 | int(data["y"].(float64)), 35 | 1, 1, 36 | ) 37 | e.SetCell(0, 0, &tl.Cell{ 38 | Ch: []rune(data["ch"].(string))[0], 39 | Fg: tl.Attr(data["color"].(float64)), 40 | }) 41 | return &Player{e} 42 | } 43 | 44 | func checkErr(err error) { 45 | if err != nil { 46 | panic(err) 47 | } 48 | } 49 | 50 | func main() { 51 | g := tl.NewGame() 52 | g.Screen().SetFps(30) 53 | l := tl.NewBaseLevel(tl.Cell{Bg: 76, Fg: 1}) 54 | lmap, err := ioutil.ReadFile("level.json") 55 | checkErr(err) 56 | parsers := make(map[string]tl.EntityParser) 57 | parsers["Player"] = parsePlayer 58 | err = tl.LoadLevelFromMap(string(lmap), parsers, l) 59 | checkErr(err) 60 | g.Screen().SetLevel(l) 61 | g.Start() 62 | } 63 | -------------------------------------------------------------------------------- /_examples/lorry.txt: -------------------------------------------------------------------------------- 1 | _______________________________________________ 2 | | | |\ 3 | | | || 4 | | TERMLOOP HAULAGE INC. |,""---:___ 5 | | ||== | .-.| 6 | | ||== | '-'-----. 7 | |_______________________________________________|| |~ | -(| 8 | |_____________________________/< _...==...____| | | ___ | 9 | \\ .-. .-. // \| \//.-. .-.\\ --------"-"/.-.\_] 10 | `( o )( o )' ' ( o )( o )`"""""""""==`( o ) 11 | '-' '-' '-' '-' '-' 12 | -------------------------------------------------------------------------------- /_examples/movingtext.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | tl "github.com/JoelOtter/termloop" 6 | "os" 7 | ) 8 | 9 | type MovingText struct { 10 | *tl.Text 11 | } 12 | 13 | func (m *MovingText) Tick(ev tl.Event) { 14 | // Enable arrow key movement 15 | if ev.Type == tl.EventKey { 16 | x, y := m.Position() 17 | switch ev.Key { 18 | case tl.KeyArrowRight: 19 | x += 1 20 | case tl.KeyArrowLeft: 21 | x -= 1 22 | case tl.KeyArrowUp: 23 | y -= 1 24 | case tl.KeyArrowDown: 25 | y += 1 26 | } 27 | m.SetPosition(x, y) 28 | } 29 | } 30 | 31 | func main() { 32 | if len(os.Args) < 2 { 33 | fmt.Println("Please provide a string as first argument") 34 | return 35 | } 36 | g := tl.NewGame() 37 | g.Screen().SetFps(30) 38 | g.Screen().AddEntity(&MovingText{tl.NewText(0, 0, os.Args[1], tl.ColorWhite, tl.ColorBlue)}) 39 | g.Start() 40 | } 41 | -------------------------------------------------------------------------------- /_examples/pyramid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | tl "github.com/JoelOtter/termloop" 5 | "math/rand" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | //////////////////////// 11 | // Maze generation stuff 12 | //////////////////////// 13 | 14 | type Point struct { 15 | x int 16 | y int 17 | p *Point 18 | } 19 | 20 | func (p *Point) Opposite() *Point { 21 | if p.x != p.p.x { 22 | return &Point{x: p.x + (p.x - p.p.x), y: p.y, p: p} 23 | } 24 | if p.y != p.p.y { 25 | return &Point{x: p.x, y: p.y + (p.y - p.p.y), p: p} 26 | } 27 | return nil 28 | } 29 | 30 | func adjacents(point *Point, maze [][]rune) []*Point { 31 | res := make([]*Point, 0) 32 | for i := -1; i < 2; i++ { 33 | for j := -1; j < 2; j++ { 34 | if (i == 0 && j == 0) || (i != 0 && j != 0) { 35 | continue 36 | } 37 | if !isInMaze(point.x+i, point.y+j, len(maze), len(maze[0])) { 38 | continue 39 | } 40 | if maze[point.x+i][point.y+j] == '*' { 41 | res = append(res, &Point{point.x + i, point.y + j, point}) 42 | } 43 | } 44 | } 45 | return res 46 | } 47 | 48 | func isInMaze(x, y int, w, h int) bool { 49 | return x >= 0 && x < w && 50 | y >= 0 && y < h 51 | } 52 | 53 | // Generates a maze using Prim's Algorithm 54 | // https://en.wikipedia.org/wiki/Maze_generation_algorithm#Randomized_Prim.27s_algorithm 55 | func generateMaze(w, h int) [][]rune { 56 | maze := make([][]rune, w) 57 | for row := range maze { 58 | maze[row] = make([]rune, h) 59 | for ch := range maze[row] { 60 | maze[row][ch] = '*' 61 | } 62 | } 63 | rand.Seed(time.Now().UnixNano()) 64 | point := &Point{x: rand.Intn(w), y: rand.Intn(h)} 65 | maze[point.x][point.y] = 'S' 66 | var last *Point 67 | walls := adjacents(point, maze) 68 | for len(walls) > 0 { 69 | rand.Seed(time.Now().UnixNano()) 70 | wall := walls[rand.Intn(len(walls))] 71 | for i, w := range walls { 72 | if w.x == wall.x && w.y == wall.y { 73 | walls = append(walls[:i], walls[i+1:]...) 74 | break 75 | } 76 | } 77 | opp := wall.Opposite() 78 | if isInMaze(opp.x, opp.y, w, h) && maze[opp.x][opp.y] == '*' { 79 | maze[wall.x][wall.y] = '.' 80 | maze[opp.x][opp.y] = '.' 81 | walls = append(walls, adjacents(opp, maze)...) 82 | last = opp 83 | } 84 | } 85 | maze[last.x][last.y] = 'L' 86 | bordered := make([][]rune, len(maze)+2) 87 | for r := range bordered { 88 | bordered[r] = make([]rune, len(maze[0])+2) 89 | for c := range bordered[r] { 90 | if r == 0 || r == len(maze)+1 || c == 0 || c == len(maze[0])+1 { 91 | bordered[r][c] = '*' 92 | } else { 93 | bordered[r][c] = maze[r-1][c-1] 94 | } 95 | } 96 | } 97 | return bordered 98 | } 99 | 100 | ///////////////// 101 | // Termloop stuff 102 | ///////////////// 103 | 104 | type Block struct { 105 | *tl.Rectangle 106 | px int // Previous x 107 | py int // Previous y 108 | move bool 109 | g *tl.Game 110 | w int // Width of maze 111 | h int // Height of maze 112 | score int 113 | scoretext *tl.Text 114 | } 115 | 116 | func NewBlock(x, y int, color tl.Attr, g *tl.Game, w, h, score int, scoretext *tl.Text) *Block { 117 | b := &Block{ 118 | g: g, 119 | w: w, 120 | h: h, 121 | score: score, 122 | scoretext: scoretext, 123 | } 124 | b.Rectangle = tl.NewRectangle(x, y, 1, 1, color) 125 | return b 126 | } 127 | 128 | func (b *Block) Draw(s *tl.Screen) { 129 | if l, ok := b.g.Screen().Level().(*tl.BaseLevel); ok { 130 | // Set the level offset so the player is always in the 131 | // center of the screen. This simulates moving the camera. 132 | sw, sh := s.Size() 133 | x, y := b.Position() 134 | l.SetOffset(sw/2-x, sh/2-y) 135 | } 136 | b.Rectangle.Draw(s) 137 | } 138 | 139 | func (b *Block) Tick(ev tl.Event) { 140 | // Enable arrow key movement 141 | if ev.Type == tl.EventKey { 142 | b.px, b.py = b.Position() 143 | switch ev.Key { 144 | case tl.KeyArrowRight: 145 | b.SetPosition(b.px+1, b.py) 146 | case tl.KeyArrowLeft: 147 | b.SetPosition(b.px-1, b.py) 148 | case tl.KeyArrowUp: 149 | b.SetPosition(b.px, b.py-1) 150 | case tl.KeyArrowDown: 151 | b.SetPosition(b.px, b.py+1) 152 | } 153 | } 154 | } 155 | 156 | func (b *Block) Collide(c tl.Physical) { 157 | if r, ok := c.(*tl.Rectangle); ok { 158 | if r.Color() == tl.ColorWhite { 159 | // Collision with walls 160 | b.SetPosition(b.px, b.py) 161 | } else if r.Color() == tl.ColorBlue { 162 | // Collision with end - new level! 163 | b.w += 1 164 | b.h += 1 165 | b.score += 1 166 | buildLevel(b.g, b.w, b.h, b.score) 167 | } 168 | } 169 | } 170 | 171 | func buildLevel(g *tl.Game, w, h, score int) { 172 | maze := generateMaze(w, h) 173 | l := tl.NewBaseLevel(tl.Cell{}) 174 | g.Screen().SetLevel(l) 175 | g.Log("Building level with width %d and height %d", w, h) 176 | scoretext := tl.NewText(0, 1, "Levels explored: "+strconv.Itoa(score), 177 | tl.ColorBlue, tl.ColorBlack) 178 | g.Screen().AddEntity(tl.NewText(0, 0, "Pyramid!", tl.ColorBlue, tl.ColorBlack)) 179 | g.Screen().AddEntity(scoretext) 180 | for i, row := range maze { 181 | for j, path := range row { 182 | if path == '*' { 183 | l.AddEntity(tl.NewRectangle(i, j, 1, 1, tl.ColorWhite)) 184 | } else if path == 'S' { 185 | col := tl.RgbTo256Color(0xff, 0, 0) 186 | l.AddEntity(NewBlock(i, j, col, g, w, h, score, scoretext)) 187 | } else if path == 'L' { 188 | l.AddEntity(tl.NewRectangle(i, j, 1, 1, tl.ColorBlue)) 189 | } 190 | } 191 | } 192 | } 193 | 194 | func main() { 195 | g := tl.NewGame() 196 | g.Screen().SetFps(60) 197 | buildLevel(g, 6, 2, 0) 198 | g.SetDebugOn(true) 199 | g.Start() 200 | } 201 | -------------------------------------------------------------------------------- /_examples/tutorial.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import tl "github.com/JoelOtter/termloop" 4 | 5 | type Player struct { 6 | *tl.Entity 7 | prevX int 8 | prevY int 9 | level *tl.BaseLevel 10 | } 11 | 12 | func (player *Player) Draw(screen *tl.Screen) { 13 | screenWidth, screenHeight := screen.Size() 14 | x, y := player.Position() 15 | player.level.SetOffset(screenWidth/2-x, screenHeight/2-y) 16 | player.Entity.Draw(screen) 17 | } 18 | 19 | func (player *Player) Tick(event tl.Event) { 20 | if event.Type == tl.EventKey { // Is it a keyboard event? 21 | player.prevX, player.prevY = player.Position() 22 | switch event.Key { // If so, switch on the pressed key. 23 | case tl.KeyArrowRight: 24 | player.SetPosition(player.prevX+1, player.prevY) 25 | case tl.KeyArrowLeft: 26 | player.SetPosition(player.prevX-1, player.prevY) 27 | case tl.KeyArrowUp: 28 | player.SetPosition(player.prevX, player.prevY-1) 29 | case tl.KeyArrowDown: 30 | player.SetPosition(player.prevX, player.prevY+1) 31 | } 32 | } 33 | } 34 | 35 | func (player *Player) Collide(collision tl.Physical) { 36 | // Check if it's a Rectangle we're colliding with 37 | if _, ok := collision.(*tl.Rectangle); ok { 38 | player.SetPosition(player.prevX, player.prevY) 39 | } 40 | } 41 | 42 | func main() { 43 | game := tl.NewGame() 44 | game.Screen().SetFps(30) 45 | level := tl.NewBaseLevel(tl.Cell{ 46 | Bg: tl.ColorGreen, 47 | Fg: tl.ColorBlack, 48 | Ch: 'v', 49 | }) 50 | level.AddEntity(tl.NewRectangle(10, 10, 50, 20, tl.ColorBlue)) 51 | player := Player{ 52 | Entity: tl.NewEntity(1, 1, 1, 1), 53 | level: level, 54 | } 55 | // Set the character at position (0, 0) on the entity. 56 | player.SetCell(0, 0, &tl.Cell{Fg: tl.ColorRed, Ch: '옷'}) 57 | level.AddEntity(&player) 58 | game.Screen().SetLevel(level) 59 | game.Start() 60 | } 61 | -------------------------------------------------------------------------------- /entity.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | // Entity provides a general Drawable to be rendered. 4 | type Entity struct { 5 | canvas Canvas 6 | x int 7 | y int 8 | width int 9 | height int 10 | } 11 | 12 | // NewEntity creates a new Entity, with position (x, y) and size 13 | // (width, height). 14 | // Returns a pointer to the new Entity. 15 | func NewEntity(x, y, width, height int) *Entity { 16 | canvas := NewCanvas(width, height) 17 | e := Entity{x: x, y: y, width: width, height: height, 18 | canvas: canvas} 19 | return &e 20 | } 21 | 22 | // NewEntityFromCanvas returns a pointer to a new Entity, with 23 | // position (x, y) and Canvas c. Width and height are calculated 24 | // using the Canvas. 25 | func NewEntityFromCanvas(x, y int, c Canvas) *Entity { 26 | e := Entity{ 27 | x: x, 28 | y: y, 29 | canvas: c, 30 | width: len(c), 31 | height: len(c[0]), 32 | } 33 | return &e 34 | } 35 | 36 | // Draw draws the entity to its current position on the screen. 37 | // This is usually called every frame. 38 | func (e *Entity) Draw(s *Screen) { 39 | for i := 0; i < e.width; i++ { 40 | for j := 0; j < e.height; j++ { 41 | s.RenderCell(e.x+i, e.y+j, &e.canvas[i][j]) 42 | } 43 | } 44 | } 45 | 46 | // Tick needs to be implemented to satisfy the Drawable interface. 47 | // It updates the Entity based on the Screen's FPS 48 | func (e *Entity) Tick(ev Event) {} 49 | 50 | // Position returns the (x, y) coordinates of the Entity. 51 | func (e *Entity) Position() (int, int) { 52 | return e.x, e.y 53 | } 54 | 55 | // Size returns the width and height of the entity, in characters. 56 | func (e *Entity) Size() (int, int) { 57 | return e.width, e.height 58 | } 59 | 60 | // SetPosition sets the x and y coordinates of the Entity. 61 | func (e *Entity) SetPosition(x, y int) { 62 | e.x = x 63 | e.y = y 64 | } 65 | 66 | // SetCell updates the attribute of the Cell at x, y to match those of c. 67 | // The coordinates are relative to the entity itself, not the Screen. 68 | func (e *Entity) SetCell(x, y int, c *Cell) { 69 | renderCell(&e.canvas[x][y], c) 70 | } 71 | 72 | // Fill fills the canvas of the Entity with 73 | // a Cell c. 74 | func (e *Entity) Fill(c *Cell) { 75 | for i := range e.canvas { 76 | for j := range e.canvas[i] { 77 | renderCell(&e.canvas[i][j], c) 78 | } 79 | } 80 | } 81 | 82 | // ApplyCanvas takes a pointer to a Canvas, c, and applies this canvas 83 | // over the top of the Entity's canvas. Any new values in c will overwrite 84 | // those in the entity. 85 | func (e *Entity) ApplyCanvas(c *Canvas) { 86 | for i := 0; i < min(len(e.canvas), len(*c)); i++ { 87 | for j := 0; j < min(len(e.canvas[0]), len((*c)[0])); j++ { 88 | renderCell(&e.canvas[i][j], &(*c)[i][j]) 89 | } 90 | } 91 | } 92 | 93 | // SetCanvas takes a pointer to a Canvas and replaces the Entity's canvas with 94 | // the pointer's. It also updates the Entity's dimensions. 95 | func (e *Entity) SetCanvas(c *Canvas) { 96 | e.width = len(*c) 97 | e.height = len((*c)[0]) 98 | e.canvas = *c 99 | } 100 | -------------------------------------------------------------------------------- /extra/audio.go: -------------------------------------------------------------------------------- 1 | package extra 2 | 3 | // extras/audio.go 4 | 5 | // This file provides types and functions for loading and playing audio. 6 | // Please note that this is in termloop/extras because it has external 7 | // dependencies - it won't work without PortAudio and libsndfile being 8 | // installed on your system. 9 | 10 | import ( 11 | "github.com/gordonklaus/portaudio" 12 | "github.com/mkb218/gosndfile/sndfile" 13 | ) 14 | 15 | // The Audio type represents the audio controller. 16 | // It should be used as a singleton, as multiple streams in PortAudio 17 | // may not be supported on some hardware. 18 | type Audio struct { 19 | tracks []*Track 20 | stream *portaudio.Stream 21 | } 22 | 23 | // The Track type represents an audio track which can be played. 24 | type Track struct { 25 | loop bool 26 | playing bool 27 | playhead int 28 | buffer []float32 29 | volume float32 30 | } 31 | 32 | // InitAudio starts up PortAudio, creates a stream and 33 | // returns a pointer to an Audio struct, or an error. 34 | func InitAudio() (*Audio, error) { 35 | a := Audio{ 36 | tracks: make([]*Track, 0), 37 | } 38 | err := portaudio.Initialize() 39 | if err != nil { 40 | return nil, err 41 | } 42 | stream, err := portaudio.OpenDefaultStream( 43 | 0, 2, float64(44100), 0, a.playCallback, 44 | ) 45 | if err != nil { 46 | return nil, err 47 | } 48 | a.stream = stream 49 | a.stream.Start() 50 | return &a, nil 51 | } 52 | 53 | // Stop shuts down PortAudio 54 | func (a *Audio) Stop() { 55 | portaudio.Terminate() 56 | } 57 | 58 | func (a *Audio) playCallback(out []float32) { 59 | for i := range out { 60 | var data float32 61 | 62 | for _, t := range a.tracks { 63 | if !t.playing { 64 | continue 65 | } 66 | if t.loop { 67 | data += (t.buffer[t.playhead%len(t.buffer)]) * t.volume 68 | t.playhead = (t.playhead + 1) % len(t.buffer) 69 | } else if t.playhead < len(t.buffer) { 70 | data += t.buffer[t.playhead] * t.volume 71 | t.playhead++ 72 | } else { 73 | t.playing = false 74 | t.playhead = 0 75 | } 76 | } 77 | out[i] = data 78 | } 79 | } 80 | 81 | // LoadTrack reads an audio track from a file, and returns a pointer to 82 | // a Track struct, or an error. The boolean parameter 'loop' determines 83 | // whether or not a Track should loop when it is finished playing. 84 | // 85 | // Supported filetypes are whatever libsndfile supports, e.g. WAV or OGG. 86 | func (a *Audio) LoadTrack(filename string, loop bool) (*Track, error) { 87 | // Load file 88 | var info sndfile.Info 89 | soundFile, err := sndfile.Open(filename, sndfile.Read, &info) 90 | if err != nil { 91 | return nil, err 92 | } 93 | buffer := make([]float32, info.Frames*int64(info.Channels)) 94 | numRead, err := soundFile.ReadItems(buffer) 95 | if err != nil { 96 | return nil, err 97 | } 98 | defer soundFile.Close() 99 | 100 | // Create track 101 | track := Track{ 102 | loop: loop, 103 | buffer: buffer[:numRead], 104 | volume: 1, 105 | } 106 | 107 | a.tracks = append(a.tracks, &track) 108 | 109 | return &track, nil 110 | } 111 | 112 | // Play triggers a Track to start playing. 113 | func (t *Track) Play() { 114 | t.playing = true 115 | } 116 | 117 | // Stop stops a Track playing, and resets it to the beginning. 118 | func (t *Track) Stop() { 119 | t.playing = false 120 | t.playhead = 0 121 | } 122 | 123 | // Pause stops a Track playing, but does not reset its position. 124 | // The track can be resumed by calling Play(). 125 | func (t *Track) Pause() { 126 | t.playing = false 127 | } 128 | 129 | // Restart resets the Track to the beginning but does not stop playback. 130 | func (t *Track) Restart() { 131 | t.playhead = 0 132 | } 133 | 134 | // Volume returns the current volume of the Track. Default is 1.0. 135 | func (t *Track) Volume() float32 { 136 | return t.volume 137 | } 138 | 139 | // SetVolume sets the Track's volume to v. 140 | func (t *Track) SetVolume(v float32) { 141 | t.volume = v 142 | } 143 | 144 | // Looping returns whether or not the Track is set to loop. 145 | func (t *Track) Looping() bool { 146 | return t.loop 147 | } 148 | 149 | // SetLooping sets whether or not a Track should loop. 150 | func (t *Track) SetLooping(looping bool) { 151 | t.loop = looping 152 | } 153 | -------------------------------------------------------------------------------- /game.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | import ( 4 | "fmt" 5 | "github.com/nsf/termbox-go" 6 | "time" 7 | ) 8 | 9 | // Represents a top-level Termloop application. 10 | type Game struct { 11 | screen *Screen 12 | input *input 13 | debug bool 14 | logs []string 15 | } 16 | 17 | // NewGame creates a new Game, along with a Screen and input handler. 18 | // Returns a pointer to the new Game. 19 | func NewGame() *Game { 20 | g := Game{ 21 | screen: NewScreen(), 22 | input: newInput(), 23 | logs: make([]string, 0), 24 | } 25 | return &g 26 | } 27 | 28 | // Screen returns the current Screen of a Game. 29 | func (g *Game) Screen() *Screen { 30 | return g.screen 31 | } 32 | 33 | // SetScreen sets the current Screen of a Game. 34 | func (g *Game) SetScreen(s *Screen) { 35 | g.screen = s 36 | g.screen.resize(termbox.Size()) 37 | } 38 | 39 | // DebugOn returns a bool showing whether or not debug mode is on. 40 | func (g *Game) DebugOn() bool { 41 | return g.debug 42 | } 43 | 44 | // SetDebugOn sets debug mode's on status to be debugOn. 45 | func (g *Game) SetDebugOn(debugOn bool) { 46 | g.debug = debugOn 47 | } 48 | 49 | // Log takes a log string and additional parameters, which can be substituted 50 | // into the string using standard fmt.Printf rules. 51 | // The formatted string is added to Game g's logs. If debug mode is on, the log will 52 | // be printed to the terminal when Termloop exits. 53 | func (g *Game) Log(log string, items ...interface{}) { 54 | toLog := "[" + time.Now().Format(time.StampMilli) + "] " + 55 | fmt.Sprintf(log, items...) 56 | g.logs = append(g.logs, toLog) 57 | } 58 | 59 | func (g *Game) dumpLogs() { 60 | if g.debug { 61 | fmt.Println("=== Logs: ===") 62 | for _, l := range g.logs { 63 | fmt.Println(l) 64 | } 65 | fmt.Println("=============") 66 | } 67 | } 68 | 69 | // SetEndKey sets the Key used to end the game. Default is KeyCtrlC. 70 | // If you don't want an end key, set it to KeyEsc, as this key 71 | // isn't supported and will do nothing. 72 | // (We recommend always having an end key for development/testing.) 73 | func (g *Game) SetEndKey(key Key) { 74 | g.input.endKey = termbox.Key(key) 75 | } 76 | 77 | // Start starts a Game running. This should be the last thing called in your 78 | // main function. By default, the escape key exits. 79 | func (g *Game) Start() { 80 | // Init Termbox 81 | err := termbox.Init() 82 | termbox.SetOutputMode(termbox.Output256) 83 | termbox.SetInputMode(termbox.InputAlt | termbox.InputMouse) 84 | if err != nil { 85 | panic(err) 86 | } 87 | defer g.dumpLogs() 88 | defer termbox.Close() 89 | g.screen.resize(termbox.Size()) 90 | 91 | // Init input 92 | g.input.start() 93 | defer g.input.stop() 94 | clock := time.Now() 95 | 96 | mainloop: 97 | for { 98 | update := time.Now() 99 | g.screen.delta = update.Sub(clock).Seconds() 100 | clock = update 101 | 102 | select { 103 | case ev := <-g.input.eventQ: 104 | if ev.Key == g.input.endKey { 105 | break mainloop 106 | } else if EventType(ev.Type) == EventResize { 107 | g.screen.resize(ev.Width, ev.Height) 108 | } else if EventType(ev.Type) == EventError { 109 | g.Log(ev.Err.Error()) 110 | } 111 | g.screen.Tick(convertEvent(ev)) 112 | default: 113 | g.screen.Tick(Event{Type: EventNone}) 114 | } 115 | 116 | g.screen.Draw() 117 | // If g.screen.fps is zero (the default), then 1000.0/g.screen.fps -> +Inf -> time.Duration(+Inf), which 118 | // is a negative number, and so time.Sleep returns immediately. 119 | time.Sleep(time.Duration((update.Sub(time.Now()).Seconds()*1000.0)+1000.0/g.screen.fps) * time.Millisecond) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | import ( 4 | "image" 5 | _ "image/gif" 6 | _ "image/jpeg" 7 | _ "image/png" 8 | "log" 9 | "os" 10 | ) 11 | 12 | // Image processing 13 | 14 | func colorGridFromFile(filename string) *[][]Attr { 15 | reader, err := os.Open(filename) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer reader.Close() 20 | m, _, err := image.Decode(reader) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | bounds := m.Bounds() 25 | 26 | // Pull pixel colour data out of image 27 | w := bounds.Max.X - bounds.Min.X 28 | h := bounds.Max.Y - bounds.Min.Y 29 | colors := make([][]Attr, w) 30 | for i := range colors { 31 | colors[i] = make([]Attr, h) 32 | } 33 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 34 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 35 | r, g, b, a := m.At(x, y).RGBA() 36 | if a < 1 { 37 | continue 38 | } 39 | R := int(r >> 8) 40 | G := int(g >> 8) 41 | B := int(b >> 8) 42 | colors[x-bounds.Min.X][y-bounds.Min.Y] = RgbTo256Color(R, G, B) 43 | } 44 | } 45 | return &colors 46 | } 47 | 48 | // BackgroundCanvasFromFile takes a path to an image file, 49 | // and creates a canvas of background-only Cells representing 50 | // the image. This can be applied to an Entity with ApplyCanvas. 51 | func BackgroundCanvasFromFile(filename string) *Canvas { 52 | colors := colorGridFromFile(filename) 53 | c := make(Canvas, len(*colors)) 54 | for i := range c { 55 | c[i] = make([]Cell, len((*colors)[i])) 56 | for j := range c[i] { 57 | c[i][j] = Cell{Bg: (*colors)[i][j]} 58 | } 59 | } 60 | return &c 61 | } 62 | 63 | // ForegroundCanvasFromFile takes a path to an image file, 64 | // and creates a canvas of foreground-only Cells representing 65 | // the image. This can be applied to an Entity with ApplyCanvas. 66 | func ForegroundCanvasFromFile(filename string) *Canvas { 67 | colors := colorGridFromFile(filename) 68 | c := make(Canvas, len(*colors)) 69 | for i := range c { 70 | c[i] = make([]Cell, len((*colors)[i])) 71 | for j := range c[i] { 72 | c[i][j] = Cell{Fg: (*colors)[i][j]} 73 | } 74 | } 75 | return &c 76 | } 77 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | import "github.com/nsf/termbox-go" 4 | 5 | type input struct { 6 | endKey termbox.Key 7 | eventQ chan termbox.Event 8 | ctrl chan bool 9 | } 10 | 11 | func newInput() *input { 12 | i := input{eventQ: make(chan termbox.Event), 13 | ctrl: make(chan bool, 2), 14 | endKey: termbox.KeyCtrlC} 15 | return &i 16 | } 17 | 18 | func (i *input) start() { 19 | go poll(i) 20 | } 21 | 22 | func (i *input) stop() { 23 | i.ctrl <- true 24 | } 25 | 26 | func poll(i *input) { 27 | loop: 28 | for { 29 | select { 30 | case <-i.ctrl: 31 | break loop 32 | default: 33 | i.eventQ <- termbox.PollEvent() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /level.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | // Level interface represents a Drawable with a separate background 4 | // that is drawn first. It can also contain Drawables of its own. 5 | type Level interface { 6 | DrawBackground(*Screen) 7 | AddEntity(Drawable) 8 | RemoveEntity(Drawable) 9 | Draw(*Screen) 10 | Tick(Event) 11 | } 12 | 13 | // BaseLevel type represents a Level with a background defined as a Cell, 14 | // which is tiled. The background is drawn first, then all Entities. 15 | type BaseLevel struct { 16 | Entities []Drawable 17 | bg Cell 18 | offsetx int 19 | offsety int 20 | } 21 | 22 | // NewBaseLevel creates a new BaseLevel with background bg. 23 | // Returns a pointer to the new BaseLevel. 24 | func NewBaseLevel(bg Cell) *BaseLevel { 25 | level := BaseLevel{Entities: make([]Drawable, 0), bg: bg} 26 | return &level 27 | } 28 | 29 | // Tick handles any collisions between Physicals in the level's entities, 30 | // and processes any input. 31 | func (l *BaseLevel) Tick(ev Event) { 32 | // Handle input 33 | for _, e := range l.Entities { 34 | e.Tick(ev) 35 | } 36 | 37 | // Handle collisions 38 | colls := make([]Physical, 0) 39 | dynamics := make([]DynamicPhysical, 0) 40 | for _, e := range l.Entities { 41 | if p, ok := interface{}(e).(Physical); ok { 42 | colls = append(colls, p) 43 | } 44 | if p, ok := interface{}(e).(DynamicPhysical); ok { 45 | dynamics = append(dynamics, p) 46 | } 47 | 48 | } 49 | jobs := make(chan DynamicPhysical, len(dynamics)) 50 | results := make(chan int, len(dynamics)) 51 | for w := 0; w <= len(dynamics)/3; w++ { 52 | go checkCollisionsWorker(colls, jobs, results) 53 | } 54 | for _, p := range dynamics { 55 | jobs <- p 56 | } 57 | close(jobs) 58 | for r := 0; r < len(dynamics); r++ { 59 | <-results 60 | } 61 | } 62 | 63 | // DrawBackground draws the background Cell bg to each Cell of the Screen s. 64 | func (l *BaseLevel) DrawBackground(s *Screen) { 65 | for i, row := range s.canvas { 66 | for j := range row { 67 | s.canvas[i][j] = l.bg 68 | } 69 | } 70 | } 71 | 72 | // Draw draws the level's entities to the Screen s. 73 | func (l *BaseLevel) Draw(s *Screen) { 74 | offx, offy := s.offset() 75 | s.setOffset(l.offsetx, l.offsety) 76 | for _, e := range l.Entities { 77 | e.Draw(s) 78 | } 79 | s.setOffset(offx, offy) 80 | } 81 | 82 | // AddEntity adds Drawable d to the level's entities. 83 | func (l *BaseLevel) AddEntity(d Drawable) { 84 | l.Entities = append(l.Entities, d) 85 | } 86 | 87 | // RemoveEntity removes Drawable d from the level's entities. 88 | func (l *BaseLevel) RemoveEntity(d Drawable) { 89 | for i, elem := range l.Entities { 90 | if elem == d { 91 | l.Entities = append(l.Entities[:i], l.Entities[i+1:]...) 92 | return 93 | } 94 | } 95 | } 96 | 97 | // Offset returns the level's drawing offset. 98 | func (l *BaseLevel) Offset() (int, int) { 99 | return l.offsetx, l.offsety 100 | } 101 | 102 | // SetOffset sets the level's drawing offset to be (x, y). 103 | // The drawing offset can be used to simulate moving the level, or 104 | // moving the 'camera'. 105 | func (l *BaseLevel) SetOffset(x, y int) { 106 | l.offsetx, l.offsety = x, y 107 | } 108 | 109 | func checkCollisionsWorker(ps []Physical, jobs <-chan DynamicPhysical, results chan<- int) { 110 | for p := range jobs { 111 | for _, c := range ps { 112 | if c == p { 113 | continue 114 | } 115 | px, py := p.Position() 116 | cx, cy := c.Position() 117 | pw, ph := p.Size() 118 | cw, ch := c.Size() 119 | if px < cx+cw && px+pw > cx && 120 | py < cy+ch && py+ph > cy { 121 | p.Collide(c) 122 | } 123 | } 124 | results <- 1 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | ) 7 | 8 | type levelMap struct { 9 | Type string 10 | Data map[string]interface{} 11 | } 12 | 13 | // An EntityParser is a function which composes an object 14 | // from data that has been parsed from a JSON file. 15 | // Returns a Drawable 16 | type EntityParser func(map[string]interface{}) Drawable 17 | 18 | func parseRectangle(data map[string]interface{}) *Rectangle { 19 | return NewRectangle( 20 | int(data["x"].(float64)), 21 | int(data["y"].(float64)), 22 | int(data["width"].(float64)), 23 | int(data["height"].(float64)), 24 | Attr(data["color"].(float64)), 25 | ) 26 | } 27 | 28 | func parseText(data map[string]interface{}) *Text { 29 | return NewText( 30 | int(data["x"].(float64)), 31 | int(data["y"].(float64)), 32 | data["text"].(string), 33 | Attr(data["fg"].(float64)), 34 | Attr(data["bg"].(float64)), 35 | ) 36 | } 37 | 38 | func parseEntity(data map[string]interface{}) (*Entity, error) { 39 | filename := data["text"].(string) 40 | text, err := ioutil.ReadFile(filename) 41 | if err != nil { 42 | return nil, err 43 | } 44 | e := NewEntityFromCanvas( 45 | int(data["x"].(float64)), 46 | int(data["y"].(float64)), 47 | CanvasFromString(string(text)), 48 | ) 49 | bgfile := data["bg"].(string) 50 | if bgfile != "" { 51 | e.ApplyCanvas(BackgroundCanvasFromFile(bgfile)) 52 | } 53 | fgfile := data["fg"].(string) 54 | if fgfile != "" { 55 | e.ApplyCanvas(ForegroundCanvasFromFile(fgfile)) 56 | } 57 | return e, nil 58 | } 59 | 60 | // LoadLevelFromMap can be used to populate a Level with entities, given 61 | // a JSON string to read from (jsonMap). 62 | // 63 | // The map 'parsers' is a map of entity names to EntityParser functions. This can 64 | // be used to define parsers for objects that are not Termloop builtins. 65 | // 66 | // The JSON string should take the format of an array of objects, like so: 67 | // [ {"type": "Rectangle", "data": {"x: 12 ...}}, ... ] 68 | // For Rectangles and Text, all attributes must be defined in the JSON. For an Entity, 69 | // fg and bg can be left as empty strings if you do not wish to color the Entity with 70 | // an image, but the keys must still be present or the parsing will break. 71 | // 72 | // There is an example of how to use this method at _examples/levelmap.go. 73 | // 74 | // LoadLevelFromMap returns an error, or nil if all is well. 75 | func LoadLevelFromMap(jsonMap string, parsers map[string]EntityParser, l Level) error { 76 | parsedMap := []levelMap{} 77 | if err := json.Unmarshal([]byte(jsonMap), &parsedMap); err != nil { 78 | return err 79 | } 80 | for _, lm := range parsedMap { 81 | var entity Drawable 82 | var err error 83 | switch lm.Type { 84 | case "Rectangle": 85 | entity = parseRectangle(lm.Data) 86 | case "Text": 87 | entity = parseText(lm.Data) 88 | case "Entity": 89 | entity, err = parseEntity(lm.Data) 90 | if err != nil { 91 | return err 92 | } 93 | default: 94 | entity = parsers[lm.Type](lm.Data) 95 | } 96 | l.AddEntity(entity) 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /rectangle.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | // A type representing a 2D rectangle, with position, size and color. 4 | type Rectangle struct { 5 | x int 6 | y int 7 | width int 8 | height int 9 | color Attr 10 | } 11 | 12 | // NewRectangle creates a new Rectangle at position (x, y), with size 13 | // (width, height) and color color. 14 | // Returns a pointer to the new Rectangle. 15 | func NewRectangle(x, y, w, h int, color Attr) *Rectangle { 16 | r := Rectangle{x: x, y: y, width: w, height: h, color: color} 17 | return &r 18 | } 19 | 20 | // Draws the Rectangle r onto Screen s. 21 | func (r *Rectangle) Draw(s *Screen) { 22 | for i := 0; i < r.width; i++ { 23 | for j := 0; j < r.height; j++ { 24 | s.RenderCell(r.x+i, r.y+j, &Cell{Bg: r.color, Ch: ' '}) 25 | } 26 | } 27 | } 28 | 29 | func (r *Rectangle) Tick(ev Event) {} 30 | 31 | // Size returns the width and height in characters of the Rectangle. 32 | func (r *Rectangle) Size() (int, int) { 33 | return r.width, r.height 34 | } 35 | 36 | // Position returns the x and y coordinates of the Rectangle. 37 | func (r *Rectangle) Position() (int, int) { 38 | return r.x, r.y 39 | } 40 | 41 | // SetPosition sets the coordinates of the Rectangle to be x and y. 42 | func (r *Rectangle) SetPosition(x, y int) { 43 | r.x = x 44 | r.y = y 45 | } 46 | 47 | // SetSize sets the width and height of the Rectangle to be w and h. 48 | func (r *Rectangle) SetSize(w, h int) { 49 | r.width = w 50 | r.height = h 51 | } 52 | 53 | // Color returns the color of the Rectangle. 54 | func (r *Rectangle) Color() Attr { 55 | return r.color 56 | } 57 | 58 | // SetColor sets the color of the Rectangle. 59 | func (r *Rectangle) SetColor(color Attr) { 60 | r.color = color 61 | } 62 | -------------------------------------------------------------------------------- /screen.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | import "github.com/nsf/termbox-go" 4 | 5 | // A Screen represents the current state of the display. 6 | // To draw on the screen, create Drawables and set their positions. 7 | // Then, add them to the Screen's Level, or to the Screen directly (e.g. a HUD). 8 | type Screen struct { 9 | oldCanvas Canvas 10 | canvas Canvas 11 | level Level 12 | Entities []Drawable 13 | width int 14 | height int 15 | delta float64 16 | fps float64 17 | offsetx int 18 | offsety int 19 | pixelMode bool 20 | } 21 | 22 | // NewScreen creates a new Screen, with no entities or level. 23 | // Returns a pointer to the new Screen. 24 | func NewScreen() *Screen { 25 | s := Screen{Entities: make([]Drawable, 0)} 26 | s.canvas = NewCanvas(10, 10) 27 | return &s 28 | } 29 | 30 | // Tick is used to process events such as input. It is called 31 | // on every frame by the Game. 32 | func (s *Screen) Tick(ev Event) { 33 | // TODO implement ticks using worker pools 34 | if s.level != nil { 35 | s.level.Tick(ev) 36 | } 37 | if ev.Type != EventNone { 38 | for _, e := range s.Entities { 39 | e.Tick(ev) 40 | } 41 | } 42 | } 43 | 44 | // Draw is called every frame by the Game to render the current 45 | // state of the screen. 46 | func (s *Screen) Draw() { 47 | // Update termloop canvas 48 | s.canvas = NewCanvas(s.width, s.height) 49 | if s.level != nil { 50 | s.level.DrawBackground(s) 51 | s.level.Draw(s) 52 | } 53 | for _, e := range s.Entities { 54 | e.Draw(s) 55 | } 56 | // Check if anything changed between Draws 57 | if !s.canvas.equals(&s.oldCanvas) { 58 | // Draw to terminal 59 | if s.pixelMode { 60 | termboxPixel(&s.canvas) 61 | } else { 62 | termboxNormal(&s.canvas) 63 | } 64 | termbox.Flush() 65 | } 66 | s.oldCanvas = s.canvas 67 | } 68 | 69 | func (s *Screen) resize(w, h int) { 70 | s.width = w 71 | s.height = h 72 | if s.pixelMode { 73 | s.height *= 2 74 | } 75 | c := NewCanvas(s.width, s.height) 76 | // Copy old data that fits 77 | for i := 0; i < min(s.width, len(s.canvas)); i++ { 78 | for j := 0; j < min(s.height, len(s.canvas[0])); j++ { 79 | c[i][j] = s.canvas[i][j] 80 | } 81 | } 82 | s.canvas = c 83 | } 84 | 85 | // Size returns the width and height of the Screen 86 | // in characters. 87 | func (s *Screen) Size() (int, int) { 88 | return s.width, s.height 89 | } 90 | 91 | // SetLevel sets the Screen's current level to be l. 92 | func (s *Screen) SetLevel(l Level) { 93 | s.level = l 94 | } 95 | 96 | // Level returns the Screen's current level. 97 | func (s *Screen) Level() Level { 98 | return s.level 99 | } 100 | 101 | // AddEntity adds a Drawable to the current Screen, to be rendered. 102 | func (s *Screen) AddEntity(d Drawable) { 103 | s.Entities = append(s.Entities, d) 104 | } 105 | 106 | // RemoveEntity removes Drawable d from the screen's entities. 107 | func (s *Screen) RemoveEntity(d Drawable) { 108 | for i, elem := range s.Entities { 109 | if elem == d { 110 | s.Entities = append(s.Entities[:i], s.Entities[i+1:]...) 111 | return 112 | } 113 | } 114 | } 115 | 116 | // TimeDelta returns the number of seconds since the previous 117 | // frame was rendered. Can be used for timings and animation. 118 | func (s *Screen) TimeDelta() float64 { 119 | return s.delta 120 | } 121 | 122 | // Set the screen framerate. By default, termloop will draw the 123 | // the screen as fast as possible, which may use a lot of system 124 | // resources. 125 | func (s *Screen) SetFps(f float64) { 126 | s.fps = f 127 | } 128 | 129 | // RenderCell updates the Cell at a given position on the Screen 130 | // with the attributes in Cell c. 131 | func (s *Screen) RenderCell(x, y int, c *Cell) { 132 | newx := x + s.offsetx 133 | newy := y + s.offsety 134 | if newx >= 0 && newx < len(s.canvas) && 135 | newy >= 0 && newy < len(s.canvas[0]) { 136 | renderCell(&s.canvas[newx][newy], c) 137 | } 138 | } 139 | 140 | // EnablePixelMode sets the screen to 'pixel mode' - giving double 141 | // the canvas height while sacrificing character drawing ability. 142 | func (s *Screen) EnablePixelMode() { 143 | s.pixelMode = true 144 | } 145 | 146 | func (s *Screen) offset() (int, int) { 147 | return s.offsetx, s.offsety 148 | } 149 | 150 | func (s *Screen) setOffset(x, y int) { 151 | s.offsetx, s.offsety = x, y 152 | } 153 | 154 | func renderCell(old, new_ *Cell) { 155 | if new_.Ch != 0 { 156 | old.Ch = new_.Ch 157 | } 158 | if new_.Bg != 0 { 159 | old.Bg = new_.Bg 160 | } 161 | if new_.Fg != 0 { 162 | old.Fg = new_.Fg 163 | } 164 | } 165 | 166 | func termboxPixel(canvas *Canvas) { 167 | for i, col := range *canvas { 168 | for j := 0; j < len(col); j += 2 { 169 | cellBack := col[j] 170 | cellFront := col[j+1] 171 | termj := j / 2 172 | char := '\u2584' 173 | if cellFront.Bg == 0 { 174 | char = 0 175 | } 176 | termbox.SetCell(i, termj, char, 177 | termbox.Attribute(cellFront.Bg), 178 | termbox.Attribute(cellBack.Bg)) 179 | } 180 | } 181 | } 182 | 183 | func termboxNormal(canvas *Canvas) { 184 | for i, col := range *canvas { 185 | for j, cell := range col { 186 | termbox.SetCell(i, j, cell.Ch, 187 | termbox.Attribute(cell.Fg), 188 | termbox.Attribute(cell.Bg)) 189 | } 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /termloop.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | "strings" 6 | ) 7 | 8 | // A Canvas is a 2D array of Cells, used for drawing. 9 | // The structure of a Canvas is an array of columns. 10 | // This is so it can be addressed canvas[x][y]. 11 | type Canvas [][]Cell 12 | 13 | // NewCanvas returns a new Canvas, with 14 | // width and height defined by arguments. 15 | func NewCanvas(width, height int) Canvas { 16 | canvas := make(Canvas, width) 17 | for i := range canvas { 18 | canvas[i] = make([]Cell, height) 19 | } 20 | return canvas 21 | } 22 | 23 | func (canvas *Canvas) equals(oldCanvas *Canvas) bool { 24 | c := *canvas 25 | c2 := *oldCanvas 26 | if c2 == nil { 27 | return false 28 | } 29 | if len(c) != len(c2) { 30 | return false 31 | } 32 | if len(c[0]) != len(c2[0]) { 33 | return false 34 | } 35 | for i := range c { 36 | for j := range c[i] { 37 | equal := c[i][j].equals(&(c2[i][j])) 38 | if !equal { 39 | return false 40 | } 41 | } 42 | } 43 | return true 44 | } 45 | 46 | // CanvasFromString returns a new Canvas, built from 47 | // the characters in the string str. Newline characters in 48 | // the string are interpreted as a new Canvas row. 49 | func CanvasFromString(str string) Canvas { 50 | lines := strings.Split(str, "\n") 51 | runes := make([][]rune, len(lines)) 52 | width := 0 53 | for i := range lines { 54 | runes[i] = []rune(lines[i]) 55 | width = max(width, len(runes[i])) 56 | } 57 | height := len(runes) 58 | canvas := make(Canvas, width) 59 | for i := 0; i < width; i++ { 60 | canvas[i] = make([]Cell, height) 61 | for j := 0; j < height; j++ { 62 | if i < len(runes[j]) { 63 | canvas[i][j] = Cell{Ch: runes[j][i]} 64 | } 65 | } 66 | } 67 | return canvas 68 | } 69 | 70 | // Drawable represents something that can be drawn, and placed in a Level. 71 | type Drawable interface { 72 | Tick(Event) // Method for processing events, e.g. input 73 | Draw(*Screen) // Method for drawing to the screen 74 | } 75 | 76 | // Physical represents something that can collide with another 77 | // Physical, but cannot process its own collisions. 78 | // Optional addition to Drawable. 79 | type Physical interface { 80 | Position() (int, int) // Return position, x and y 81 | Size() (int, int) // Return width and height 82 | } 83 | 84 | // DynamicPhysical represents something that can process its own collisions. 85 | // Implementing this is an optional addition to Drawable. 86 | type DynamicPhysical interface { 87 | Position() (int, int) // Return position, x and y 88 | Size() (int, int) // Return width and height 89 | Collide(Physical) // Handle collisions with another Physical 90 | } 91 | 92 | func min(a, b int) int { 93 | if a < b { 94 | return a 95 | } 96 | return b 97 | } 98 | 99 | func max(a, b int) int { 100 | if a > b { 101 | return a 102 | } 103 | return b 104 | } 105 | 106 | // Abstract Termbox stuff for convenience - users 107 | // should only need Termloop imported 108 | 109 | // Represents a character to be drawn on the screen. 110 | type Cell struct { 111 | Fg Attr // Foreground colour 112 | Bg Attr // Background color 113 | Ch rune // The character to draw 114 | } 115 | 116 | func (c *Cell) equals(c2 *Cell) bool { 117 | return c.Fg == c2.Fg && 118 | c.Bg == c2.Bg && 119 | c.Ch == c2.Ch 120 | } 121 | 122 | // Provides an event, for input, errors or resizing. 123 | // Resizing and errors are largely handled by Termloop itself 124 | // - this would largely be used for input. 125 | type Event struct { 126 | Type EventType // The type of event 127 | Key Key // The key pressed, if any 128 | Ch rune // The character of the key, if any 129 | Mod Modifier // A keyboard modifier, if any 130 | Err error // Error, if any 131 | MouseX int // Mouse X coordinate, if any 132 | MouseY int // Mouse Y coordinate, if any 133 | } 134 | 135 | func convertEvent(ev termbox.Event) Event { 136 | return Event{ 137 | Type: EventType(ev.Type), 138 | Key: Key(ev.Key), 139 | Ch: ev.Ch, 140 | Mod: Modifier(ev.Mod), 141 | Err: ev.Err, 142 | MouseX: ev.MouseX, 143 | MouseY: ev.MouseY, 144 | } 145 | } 146 | 147 | type ( 148 | Attr uint16 149 | Key uint16 150 | Modifier uint8 151 | EventType uint8 152 | ) 153 | 154 | // Types of event. For example, a keyboard press will be EventKey. 155 | const ( 156 | EventKey EventType = iota 157 | EventResize 158 | EventMouse 159 | EventError 160 | EventInterrupt 161 | EventRaw 162 | EventNone 163 | ) 164 | 165 | // Cell colors. You can combine these with multiple attributes using 166 | // a bitwise OR ('|'). Colors can't combine with other colors. 167 | const ( 168 | ColorDefault Attr = iota 169 | ColorBlack 170 | ColorRed 171 | ColorGreen 172 | ColorYellow 173 | ColorBlue 174 | ColorMagenta 175 | ColorCyan 176 | ColorWhite 177 | ) 178 | 179 | // Cell attributes. These can be combined with OR. 180 | const ( 181 | AttrBold Attr = 1 << (iota + 9) 182 | AttrUnderline 183 | AttrReverse 184 | ) 185 | 186 | const ModAltModifier = 0x01 187 | 188 | // Key constants. See Event.Key. 189 | const ( 190 | KeyF1 Key = 0xFFFF - iota 191 | KeyF2 192 | KeyF3 193 | KeyF4 194 | KeyF5 195 | KeyF6 196 | KeyF7 197 | KeyF8 198 | KeyF9 199 | KeyF10 200 | KeyF11 201 | KeyF12 202 | KeyInsert 203 | KeyDelete 204 | KeyHome 205 | KeyEnd 206 | KeyPgup 207 | KeyPgdn 208 | KeyArrowUp 209 | KeyArrowDown 210 | KeyArrowLeft 211 | KeyArrowRight 212 | key_min 213 | MouseLeft 214 | MouseMiddle 215 | MouseRight 216 | MouseRelease 217 | MouseWheelUp 218 | MouseWheelDown 219 | ) 220 | 221 | const ( 222 | KeyCtrlTilde Key = 0x00 223 | KeyCtrl2 Key = 0x00 224 | KeyCtrlSpace Key = 0x00 225 | KeyCtrlA Key = 0x01 226 | KeyCtrlB Key = 0x02 227 | KeyCtrlC Key = 0x03 228 | KeyCtrlD Key = 0x04 229 | KeyCtrlE Key = 0x05 230 | KeyCtrlF Key = 0x06 231 | KeyCtrlG Key = 0x07 232 | KeyBackspace Key = 0x08 233 | KeyCtrlH Key = 0x08 234 | KeyTab Key = 0x09 235 | KeyCtrlI Key = 0x09 236 | KeyCtrlJ Key = 0x0A 237 | KeyCtrlK Key = 0x0B 238 | KeyCtrlL Key = 0x0C 239 | KeyEnter Key = 0x0D 240 | KeyCtrlM Key = 0x0D 241 | KeyCtrlN Key = 0x0E 242 | KeyCtrlO Key = 0x0F 243 | KeyCtrlP Key = 0x10 244 | KeyCtrlQ Key = 0x11 245 | KeyCtrlR Key = 0x12 246 | KeyCtrlS Key = 0x13 247 | KeyCtrlT Key = 0x14 248 | KeyCtrlU Key = 0x15 249 | KeyCtrlV Key = 0x16 250 | KeyCtrlW Key = 0x17 251 | KeyCtrlX Key = 0x18 252 | KeyCtrlY Key = 0x19 253 | KeyCtrlZ Key = 0x1A 254 | KeyEsc Key = 0x1B // No longer supported 255 | KeyCtrlLsqBracket Key = 0x1B 256 | KeyCtrl3 Key = 0x1B 257 | KeyCtrl4 Key = 0x1C 258 | KeyCtrlBackslash Key = 0x1C 259 | KeyCtrl5 Key = 0x1D 260 | KeyCtrlRsqBracket Key = 0x1D 261 | KeyCtrl6 Key = 0x1E 262 | KeyCtrl7 Key = 0x1F 263 | KeyCtrlSlash Key = 0x1F 264 | KeyCtrlUnderscore Key = 0x1F 265 | KeySpace Key = 0x20 266 | KeyBackspace2 Key = 0x7F 267 | KeyCtrl8 Key = 0x7F 268 | ) 269 | -------------------------------------------------------------------------------- /text.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | // Text represents a string that can be drawn to the screen. 4 | type Text struct { 5 | x int 6 | y int 7 | fg Attr 8 | bg Attr 9 | text []rune 10 | canvas []Cell 11 | } 12 | 13 | // NewText creates a new Text, at position (x, y). It sets the Text's 14 | // background and foreground colors to fg and bg respectively, and sets the 15 | // Text's text to be text. 16 | // Returns a pointer to the new Text. 17 | func NewText(x, y int, text string, fg, bg Attr) *Text { 18 | str := []rune(text) 19 | c := make([]Cell, len(str)) 20 | for i := range c { 21 | c[i] = Cell{Ch: str[i], Fg: fg, Bg: bg} 22 | } 23 | return &Text{ 24 | x: x, 25 | y: y, 26 | fg: fg, 27 | bg: bg, 28 | text: str, 29 | canvas: c, 30 | } 31 | } 32 | 33 | func (t *Text) Tick(ev Event) {} 34 | 35 | // Draw draws the Text to the Screen s. 36 | func (t *Text) Draw(s *Screen) { 37 | w, _ := t.Size() 38 | for i := 0; i < w; i++ { 39 | s.RenderCell(t.x+i, t.y, &t.canvas[i]) 40 | } 41 | } 42 | 43 | // Position returns the (x, y) coordinates of the Text. 44 | func (t *Text) Position() (int, int) { 45 | return t.x, t.y 46 | } 47 | 48 | // Size returns the width and height of the Text. 49 | func (t *Text) Size() (int, int) { 50 | return len(t.text), 1 51 | } 52 | 53 | // SetPosition sets the coordinates of the Text to be (x, y). 54 | func (t *Text) SetPosition(x, y int) { 55 | t.x = x 56 | t.y = y 57 | } 58 | 59 | // Text returns the text of the Text. 60 | func (t *Text) Text() string { 61 | return string(t.text) 62 | } 63 | 64 | // SetText sets the text of the Text to be text. 65 | func (t *Text) SetText(text string) { 66 | t.text = []rune(text) 67 | c := make([]Cell, len(t.text)) 68 | for i := range c { 69 | c[i] = Cell{Ch: t.text[i], Fg: t.fg, Bg: t.bg} 70 | } 71 | t.canvas = c 72 | } 73 | 74 | // Color returns the (foreground, background) colors of the Text. 75 | func (t *Text) Color() (Attr, Attr) { 76 | return t.fg, t.bg 77 | } 78 | 79 | // SetColor sets the (foreground, background) colors of the Text 80 | // to fg, bg respectively. 81 | func (t *Text) SetColor(fg, bg Attr) { 82 | t.fg = fg 83 | t.bg = bg 84 | for i := range t.canvas { 85 | t.canvas[i].Fg = fg 86 | t.canvas[i].Bg = bg 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package termloop 2 | 3 | import "strconv" 4 | 5 | // Utility types and associated methods 6 | 7 | // FpsText provides a Text which updates with the current 'framerate' 8 | // at specified intervals, to be used for testing performance. 9 | // Please note that the framerate displayed is a measure of Termloop's 10 | // processing speed - visible framerate is largely dependent on your terminal! 11 | type FpsText struct { 12 | *Text 13 | time float64 14 | update float64 15 | } 16 | 17 | // NewFpsText creates a new FpsText at position (x, y) and with background 18 | // and foreground colors fg and bg respectively. It will refresh every 19 | // 'update' seconds. 20 | // Returns a pointer to the new FpsText. 21 | func NewFpsText(x, y int, fg, bg Attr, update float64) *FpsText { 22 | return &FpsText{ 23 | Text: NewText(x, y, "", fg, bg), 24 | time: 0, 25 | update: update, 26 | } 27 | } 28 | 29 | // Draw updates the framerate on the FpsText and draws it to the Screen s. 30 | func (f *FpsText) Draw(s *Screen) { 31 | f.time += s.TimeDelta() 32 | if f.time > f.update { 33 | fps := strconv.FormatFloat(1.0/s.TimeDelta(), 'f', 10, 64) 34 | f.SetText(fps) 35 | f.time -= f.update 36 | } 37 | f.Text.Draw(s) 38 | } 39 | 40 | func cubeIndex(x int, points [5]int) int { 41 | n := 0 42 | for _, p := range points { 43 | if x <= p { 44 | break 45 | } else { 46 | n++ 47 | } 48 | } 49 | return n 50 | } 51 | 52 | // RgbTo256Color takes RGB values and returns the closest color for 53 | // a 256-color terminal, as an Attr type. 54 | func RgbTo256Color(r, g, b int) Attr { 55 | cubepoints := [5]int{47, 115, 155, 195, 235} 56 | r256 := cubeIndex(r, cubepoints) 57 | g256 := cubeIndex(g, cubepoints) 58 | b256 := cubeIndex(b, cubepoints) 59 | return Attr(r256*36 + g256*6 + b256 + 17) 60 | } 61 | --------------------------------------------------------------------------------