Mirage is a small 2D game engine written in 24 hours, because why not? It's definitely not perfect but it's usable, and it's the first complete game engine I've ever made.
Support me: https://www.patreon.com/n8dev
Watch the devlog: https://youtu.be/hysfq_xJAw0
Join my Discord server for help: https://discord.gg/f8B6WW7YrD
- Runs on Windows exclusively because of System.Drawing :/
- Built and used with C# and .NET 6
- 2D sprite rendering with OpenGL (Silk.NET bindings)
- Keyboard inputs
- A beautiful and easy-to-use API
- An API that's also very extendable if you wanna put in the work
-
Create a new .NET 6 Console application (here's how if you don't know)
-
Install the NateCurtiss.Mirage NuGet package (here's how if you've never installed NuGet packages before)
-
You're done :D
You'll find some sample projects in the repository, prefixed with Sample
. For now there's Flappy Bird and Pong. Controls for both are pretty self-explanatory - just use WASD/Arrows and Space for everything.
Just like everything else, the game art and color palettes are under the MIT License so feel free to use those for whatever you want WITH or WITHOUT credit - it's up to you (although credit is always nice :p).
After adding the NuGet package to your project, create a file called Program.cs
with either a top-level statement or Main
method, and then create a new Game
and Start()
it.
new Game().Start();
The Game
class takes in a few arguments in its constructor, so let's create those. Start with the Window
, passing in a title
, width
, height
and optionally whether the path to a custom Window
Icon
, the Color
to use the for Window's
background, and/or whether it should maximized
and/or resizable
.
var window = new Window("If you can read this you don't need glasses.", 1920, 1080, maximized: true);
new Game(window).Start();
Next we'll need the other arguments, so create the Keyboard
...
var window = new Window("If you can read this you don't need glasses.", 1920, 1080, maximized: true);
var keyboard = new Keyboard();
new Game(window, keyboard).Start();
the Graphics
object, which acts as the wrapper for OpenGL....
var window = new Window("If you can read this you don't need glasses.", 1920, 1080, maximized: true);
var keyboard = new Keyboard();
var graphics = new Graphics();
new Game(window, keyboard, graphics).Start();
the Camera
, passing in the Window
...
var window = new Window("If you can read this you don't need glasses.", 1920, 1080, maximized: true);
var keyboard = new Keyboard();
var graphics = new Graphics();
var camera = new Camera(window);
new Game(window, keyboard, graphics).Start();
the Renderer
, passing in the Camera
and the Window
...
var window = new Window("If you can read this you don't need glasses.", 1920, 1080, maximized: true);
var keyboard = new Keyboard();
var graphics = new Graphics();
var camera = new Camera(window);
var renderer = new Renderer(camera, window);
new Game(window, keyboard, graphics, renderer).Start();
and finally, the World
, which contains all of the Entities
in the Game
. You'll need to pass in everything to this.
var window = new Window("If you can read this you don't need glasses.", 1920, 1080, maximized: true);
var keyboard = new Keyboard();
var graphics = new Graphics();
var camera = new Camera(window);
var renderer = new Renderer(camera, window);
var world = new World(window, keyboard, graphics, camera, renderer);
new Game(world, window, keyboard, graphics, renderer).Start();
Now if we run our application we should get a blank Window
with a title and icon!
A "thing" in the World
is called an Entity
; let's create one! First let's create a new file in our project called Player.cs
, and make that class inherit from Entity
.
class Player : Entity
{
}
Entities
have a set of "event methods" called at different times at different frequencies that can be overriden. Here's a brief explanation of all of them.
OnAwake()
: called BEFORE the first frame of theEntity
's lifetime; use this for initializing variables and event handlingOnStart()
: called ON the first frame of theEntity
's lifetime; use this for game logic that should run on the first frameOnKill()
: called when theEntity
is killed.`OnUpdate(float deltaTime)
: called every frame
Simply override any of the event methods to have your Entity
receive callbacks.
class Player : Entity
{
protected override void OnStart()
{
Console.WriteLine("The Game has started lol.");
}
protected override void OnUpdate(float deltaTime)
{
Console.WriteLine("The Game has updated lmao.");
}
}
Spawning an Entity
is just as easy. To spawn an Entity
we need to go through the World
first, as that's where Entities
live. Back in our Program.cs
file we have a reference to the World
, so let's spawn in our Player
there.
var world = new World(window, keyboard, graphics, camera, renderer).Spawn<Player>();
That's it! The Spawn<T>()
method takes in a type parameter T
, which is just the type of Entity
we'd like to spawn (in this case: Player
). World.Spawn<T>()
returns the World
so that we can chain these as much as we want, which makes it look hella pretty.
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Player>()
.Spawn<Enemy>()
.Spawn<Enemy>()
.Spawn<Floor>()
.Spawn<GameManager>()
// ...
In literally every single video game ever developed, objects depend on each other. Mirage is code-only, so there's no drag-and-drop visual editor like Unity or Godot, but there are still a few good ways to resolve dependencies.
There's an overload for World.Spawn<T>()
that takes in an argument to output the spawned Entity
of type T
.
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Enemy>(out var enemy) // The Enemy needs to know where the Player is to follow them.
.Spawn<Player>(out var player);
We can then do stuff to this Entity
by chaining a World.OnAwake()
or World.OnStart()
call. These two methods act just like Entity.OnAwake()
Entity.OnStart()
, but are called after every single Entity
has received the callback for the corresponding event method.
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Enemy>(out var enemy)
.Spawn<Player>(out var player)
.OnAwake() => // Called after Enemy.OnAwake() and Player.OnAwake().
{
enemy.Target = player;
};
Note: a World.OnUpdate(float deltaTime)
callback also exists.
Sometimes you'll want to pass in a lot of simple values, and something like
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Player>(out var player)
.OnAwake() =>
{
player.Speed = 1f;
player.Jump = 5.5f;
player.Height = 20f;
player.ShouldLick = true;
player.IsSubscribedToN8Dev = true;
player.Pants = new Pants(Jeans.Good);
player.EyeColor = Eyes.Green;
// ...
};
just isn't gonna cut it.
Instead we can have Player
inherit from Entity<T>
like so.
class Player : Entity<float>
{
}
This gives us an extra method, OnConfigure(T config)
which looks like this in our Player
.
class Player : Entity<float>
{
protected override void OnConfigure(float config)
{
}
}
This method is special because it allows us to pass values in when we spawn in the Entity
. In this case we're passing in the _moveSpeed
of the player, so we'd use it like this.
class Player : Entity<float>
{
float _moveSpeed;
protected override void OnConfigure(float config)
{
_moveSpeed = config;
}
}
To pass in our _moveSpeed
we'll need to go back to our main file and use a different overload of the Spawn<T>()
method.
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Player, float>(5f);
All we're doing here is telling the World
that
- A: we're spawning in an
Entity
of typePlayer
- B: we're passing in a
float
to itsOnConfigure
method - and C: we want that
float
to be equal to 5
And now we've given our Player
a speed of 5! So now if we have multiple Players
we can easily tweak values to our liking.
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Player, float>(1f)
.Spawn<Player, float>(0.5f); // Player 2 will be slower.
.Spawn<Player, float>(100f); // Player 3 just drank some Red Bull.
Here's that example with the enemy from earlier.
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Player>(out var player)
.Spawn<Enemy, Player>(player);
Much more elegant.
"But Nate..." I hear you ask, "What if I have, for example, a bunch of weapons that have multiple properties I'd like to tweak per instance? This way only allows me to pass in a single argument to an Entity
."
Well, you're right...in a way, but there's a pretty simple workaround. To fix this we can just create a struct that's something like this.
struct WeaponConfig
{
public string Name;
public float Power;
public float Range;
// ...
}
And pass THAT into our Entity
.
class Weapon : Entity<WeaponConfig>
{
float _name;
float _power;
float _range;
protected override void OnConfigure(WeaponConfig config)
{
_name = config.Name;
_power = config.Power;
_range = config.Range;
}
}
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Weapon, WeaponConfig>(new("sword", 100f, 5f))
.Spawn<Weapon, WeaponConfig>(new("spear", 30f, 30f))
.Spawn<Weapon, WeaponConfig>(new("club", 200f, 2f))
// ...
Even better, we can still get the Entity
spawned through another overload of the Spawn<TE, TC>()
method.
var world = new World(window, keyboard, graphics, camera, renderer)
.Spawn<Weapon, WeaponConfig>(new("sword", 100f, 5f), out var sword)
.Spawn<Player, Weapon>(sword);
Entities
have access to all those modules we created at the start (Window
, Keyboard
, etc)
class Player : Entity<float>
{
float _moveSpeed;
protected override void OnConfigure(float config)
{
_moveSpeed = config;
}
protected override void OnUpdate(float deltaTime)
{
if (Keyboard.IsDown(Key.RightArrow))
Position += new Vector2(_moveSpeed, 0f);
}
}
There's no physics engine because I wrote this in 24 hours, but you can emulate collisions with Entity.Bounds
class Enemy : Entity<Player>
{
Player _player
protected override void OnConfigure(Player config)
{
_player = player;
}
protected override void OnUpdate(float deltaTime)
{
if (Bounds.Overlaps(_player.Bounds))
World.Kill(_player);
}
}
And...that's about it. Check out the sample projects in the repo for further guidance on the parts of the API I didn't talk about and don't hesitate to ask for help in my Discord server.
Mirage is under the MIT License which gives you the freedom to do pretty much whatever you want with the engine; every game you make with Mirage is 100% yours down to the very last semicolon.