this banner graphic was rendered and displayed inside a Windows Command Prompt using Asciir.
The Ascii Renderer project, Asciir for short, is a Renderer / Game engine that uses the terminal / console as its window. It makes use of ANSI control sequences in order to control the terminal.
Displaying graphics in a terminal can often be very tedious and inconvenient, as it was not really designed for it. This is what Asciir aims to eliminate, by providing a library that allows the user to render graphics in the terminal with ease. It allows one to easily display graphics in the terminal as well as control the terminal window size (depending on the terminal), title and other properties. As Asciir also is intended to be used as a game engine, it also aims to provide the highest frame rate possible, for the given task.
There exists of two primary branches (master and dev) and an additional collection of feature branches.
Contains the latest stable(ish) code.
Contains all small features / fixes that has not yet been merged into the master branch or is still being worked on.
Each feature branch contains code for a larger feature that is currently being developed. These branches should be merged into dev when they are fully developed.
here are some demo projects build with Asciir, see the examples folder for more examples.
simply displays three yellow triangles that move and rotate on the screen. The framerate is displayed as the title.
This example projects contains all the code that was used to generate the banner graphic displayed at the top of the README. It makes use of texture loading and more complex shaders for the lightning in the background and some slight enhancements to loaded textures.
-
3rd party
- Eigen (linear algebra)
- CImg (loading images)
- zlib (uncompressing files)
- SFML (audio)
- FastNoise2 (noise generation)
- libpng (reading png files)
- libjpeg-turbo (reading jpg files)
-
My projects
- EThread
- ChrTrcProfiler (visual profiling)
Currently, Asciir only works on the Windows platform, however cross platform support is planned to be implemented some time in the future.
if using ASCIIR_CONAN_AUTO_INSTALL, conan is required
- Conan
pip install conan
Asciir uses CMake as its build system and conan to handle the download of some dependencies.
-
Clone the Asciir repositry (remember --recursive)
git clone --recursive https://github.com/karstensensensen/AsciiRenderer
-
cd into the cloned repositry in a terminal
cd Asciir
-
Configure cmake (omit -DASCIIR_AUTO_INSTALL_DEPS if conan should not be used)
mkdir build cmake -DASCIIR_AUTO_INSTALL_DEPS=ON -S . -B ./build
-
build the library (this may take a while the first time)
cmake --build ./build
now you can link your project to the build static library. As mentioned earlier, a OpenAL dll is required if audio is used at any point.
Asciir exposes a target inside CMake any external projects can link to, in order to use Asciir in their build system.
- Start by adding Asciir as a submodule inside your project folder structure
git submodule add https://github.com/karstensensensen/AsciiRenderer
- add the following lines to your CMakeLists.txt
... add_subdirectory("${CMAKE_CURRENT_SRC_DIR}/.../Asciir") target_link_libraries(${PROJECT_NAME} Asciir::Asciir) ...
now your project should be able to include the Asciir headers as well as link to the Asciir libraries. For more details see the CMake section.
Each Asciir project starts by creating an ARApp that pushes at least one Layer onto its layerstack. Inside this Layer, functions like onAdd, onRemove and onUpdate can be overridden in order to implement application logic.
#include <Asciir.h>
using namespace Asciir;
// _R
using namespace Asciir::AsciirLiterals;
class MyLayer : public Layer
{
public:
// on update gets called every frame, and should handle any game / application logic as well as submitting any Graphical structures for rendering, using Renderer::submit. (will be replaced with an Renderer system in the future)
virtual void onUpdate(DeltaTime dt) override
{
}
};
class MyApplication : public ARApp
{
public:
// Called once when the Asciir library has been initialized and is ready to start.
// args contains the command line arguments.
virtual void start(const std::vector<std::string>& args) override
{
// push MyLayer onto the layer stack.
pushLayer(new MyLayer());
}
};
// you can write your own main function and use AsciiInit inside it as an alternative to this
AR_DEFAULT_ENTRYPOINT(MyApplication)
Now we can set up the renderer as well as set the title of our application. This is done through the Renderer interface.
...
class MyApplication : public ARApp
{
public:
// Called once when the Asciir library has been initialized and is ready to start.
// args contains the command line arguments.
virtual void start(const std::vector<std::string>& args) override
{
// push MyLayer onto the layer stack.
pushLayer(new MyLayer());
// make sure the Renderer uses multiple threads when rendering frames.
Renderer::setThreads();
// set the title of the terminal
Renderer::setTitle("My Cool Terminal App");
// limit the framerate to 60 fps.
Renderer::setMinDT(DeltaTime(60).fps());
}
};
...
Now our application just need to actually do something. In this example we will create a blue box that can be controlled by the arrow keys on the keyboard. So lets start by adding some variables.
class MyLayer : public Layer
{
public:
...
// the speed of the cube when moving.
// Real = datatype for representing floating point numbers.
// should be used instead of float or double.
// _R makes sure the literal is of the Real data type.
Real speed = 100_R;
// the position of the cube on the screen, 1 unit = 1 terminal character
// Coord a 2D vector using Real as its underlying data type
Coord cube_pos = { 0, 0 };
// the width and height of the cube, 1 unit = 1 terminal character
// Size2D a 2D vector using size_t as its underlying data type
Size2D cube_size = { 10, 10 };
// the colour of the box. Here we use a constant provided by the Asciir library for a standard blue colour in a terminal.
// Colour represents a 4 byte RGBA value
Colour cube_colour = IBLUE8;
...
};
Finally, we need to move the cube around and render it to the screen. We do this by using the Input interface in order to check for key presses, and the Renderer interface to submit a rectangle to the renderer.
class MyLayer : public Layer
{
...
// on update gets called every frame, and should handle any game / application logic as well as submitting any Graphical structures for rendering, using Renderer::submit. (will be replaced with an Renderer system in the future)
virtual void onUpdate(DeltaTime dt) override
{
// modify the position, depending on which keys are currently pressed.
// use the Input static class to access the user Input interface.
// here we multiply by "dt". dt is the time that has passed since the last call to update, so by multiplying by this value, we make sure our velocity is independent of framerate.
if (Input::isKeyDown(Key::LEFT))
cube_pos.x -= speed * dt;
if (Input::isKeyDown(Key::RIGHT))
cube_pos.x += speed * dt;
if (Input::isKeyDown(Key::UP))
cube_pos.y -= speed * dt;
if (Input::isKeyDown(Key::DOWN))
cube_pos.y += speed * dt;
// use the submitRect helper function to quickly submit a rectangle by submitting its top right and bottom left corner, as well as its colour.
Renderer::submitRect(s_Coords<2>({ cube_pos, cube_pos + (Coord)cube_size }), Tile(cube_colour));
}
...
};
The final code should look something like this.
#include <Asciir.h>
using namespace Asciir;
// _R
using namespace Asciir::AsciirLiterals;
class MyLayer : public Layer
{
public:
Real speed = 100_R;
Coord cube_pos = { 0, 0 };
Size2D cube_size = { 10, 10 };
Colour cube_colour = IBLUE8;
virtual void onUpdate(DeltaTime dt) override
{
if (Input::isKeyDown(Key::LEFT))
cube_pos.x -= speed * dt;
if (Input::isKeyDown(Key::RIGHT))
cube_pos.x += speed * dt;
if (Input::isKeyDown(Key::UP))
cube_pos.y -= speed * dt;
if (Input::isKeyDown(Key::DOWN))
cube_pos.y += speed * dt;
Renderer::submitRect(s_Coords<2>({ cube_pos, cube_pos + (Coord)cube_size }), Tile(cube_colour));
}
};
class MyApplication : public ARApp
{
public:
virtual void start(const std::vector<std::string>& args) override
{
pushLayer(new MyLayer());
Renderer::setThreads();
Renderer::setTitle("My Cool Terminal App");
Renderer::setMinDT(DeltaTime(60).fps());
}
};
// you can write your own main function and use AsciiInit inside it as an alternative to this
AR_DEFAULT_ENTRYPOINT(MyApplication)
More tutorials will come in the future, in the meantime checkout the codebase reference to discover some of Asciirs features by yourself!
The following variables are exposed and are initialized with the displayed default values.
ASCIIR_LOG_VIEWER = ON
ASCIIR_EXAMPLES = OFF
ASCIIR_HIGH_PRECISSION_FLOAT = OFF
ASCIIR_AUTO_INSTALL_DEPS = OFF
ASCIIR_PARALLEL_BUILD = ON
Builds a executable that is able to display Asciir log files. As a process can only own 1 terminal at a time (on Windows at least), a seperate program is needed for logging, as the primary program is already using the terminal for displaying graphics.
Builds all the example projects in the examples folder.
Use double instead of float for the Real typedef.
Automatically installs all the dependencies using the Conan package manager. (conan is required for this to work)
The alias target Asciir::Asciir
can be used if one wants to link to the Asciir library through CMake.
Builds the Asciir library using multiple threads.
This should be enabled for a faster build time, at the cost of a higher CPU load.
The slowest process during the rendering is by far actually printing the new graphics to the terminal. As the terminal does not erase its contents everyframe, only modified tiles, compared to the previous frame, are actually printed to the terminal. If a high fps is required, this optimization should be kept in mind, as one can still have a relatively complex scene rendered and still have a high frame rate, as long as the modified tiles are kept to a minnimum.
An example of an optimization could be the rendering of an expanding gradient. Event though each tile might only have its colour values changed by relatively small values, they still have to be redrawn. In order to optimize this example, we instead need to reduce the number of tiles changed pr. frame. This can be done by intentionally introducing banding in the gradient's colours, as this will reduce the number of times a tile will change colour, and thus reduce the number of writes to the terminal.
- Make README more nice
- Improve documentation + tutorials
- Add built in Systems and Components (Script system, Renderer system, Physics system...)
- Cleanup CMake file
- Implement all possible character attributes for the Tile class
- Integrate the Entity Component System into the game engine
- Create a demo game
- Refactor some of the code base
- Implement a tree based entity structure (like godot)
- Create a Texture editor
- Cross platform support
- Graphical User interface tools (widgets and stuff)
- Create a Game Engine editor
Distributed under the MIT License. See LICENSE
for more information.
This game engine was built with the help of The Cherno's game engine series. I highly recommend it as a good starting point for creating your very own game engine!