Branch | Status |
---|---|
main |
Puffin is a cross-platform 2D C# game engine built on top of MonoGame. It ships with:
- An extensible entity/component system
- Pre-made components for images, spritesheets, audio, mouse, and keyboard input
- Dynamic font loading/resizing at runtime
- 2D tilemaps
- Fast AABB collision resolution for entities and tilemaps
This project is no longer under active development. The ECS architecture I used contains a major flaw: limiting entities to one of each component type (for performance reasons: look-up is O(1)) means it nearly impossible to make complex UI components with multiple images, and it lacks nesting entities within entities. To address these problems, I created a prototype in the Puffin v2 repository.
- Focus on your game logic, not low-level plumbing
- Focused and tailored for 2D games
- Runs and makes games for Linux, Windows, and Mac
- Easy to get started and low learning-curve
- Leverage your existing C# skills and the .NET ecosystem
- Add this repository as a
git submodule
within your project (NuGet packages are not available yet) - Build it by running
dotnet build
. Note: you will need .NET SDK 3.1 and MonoGame. - Run
dotnet new console --name MyGame
to create a new game project - Add the relevant references to Puffin:
dotnet add MyGame reference Puffin\Puffin.Core
dotnet add MyGame reference Puffin\Puffin.Infrastructure.MonoGame
dotnet add MyGame package MonoGame.Framework.DesktopGL.Core
- Download the
Open Sans
font from Google Fonts and add the-Regular.ttf
toMyGame/Content
- Create a new solution with
dotnet new sln --name <solution name>
and addMyGame
and thePuffin.Core
andPuffin.Infrastructure.MonoGame
projects to it
Puffin uses an entity-component architecture where components represent functionality (like a sprite to draw or a text/label to display on-screen). Components live in "screens" which represent screens of your game (eg. main game screen, inventory screen).
To get started, create a new game class that extends PuffinGame
and sets the game window size:
class MyGame : PuffinGame
{
public MyGame() : base(960, 540)
{
}
}
Next, create a new scene and add functionality to it:
- Create a new
FirstScene
class that extendsPuffin.Core.Scene
- Add a constructor which calls
this.Add(new Entity().Label("Hello from Puffin!"));
- Modify your game class to override
protected void Ready
and callthis.ShowScene(new FirstScene())
- Type
dotnet run
or press F5 in VSCode. It should run and you should seeHello from Puffin!
To add an image:
this.Add(new Entity().Add(new SpriteComponent("Bird.png")));
This creates a new entity, adds a SpriteComponent
to it which should appear with the image of Bird.png
, and adds the entity to the screen. When the game runs, it renders that entity at its position.
Puffin includes a separate Puffin.UI
assembly which includes UI controls. Note that they rely on images in Content/Puffin/UI
to work.
A button with an image, text label, and on-click event handler. To create:
var button = new Button(true, "Click me!", () => this.points++).Move(16, 16);
This creates a button with the caption Click me!
Clicking the button increments the local variable points
by one. The button sits at (16, 16)
on screen.
To reskin the button, change the Content/Puffin/UI/Button.png
image to something else. Note that you cannot yet resize the image.
For keyboard input, Puffin doesn't expose key-press information directly; instead, it exposes information about PuffinAction
s. Each PuffinAction
maps to one or more keys (eg. by default, the W
key maps to PuffinAction.Up
).
This allows developers/players to arbitrarily rebind actions keys without rewriting lots of code.
To create your own custom actions, simply create a new Enum
and add actions to your game constructor, like so:
// MyGame.cs
class MyGame : PuffinGame {
public enum CustomAction {
Next,
Previous,
}
public MyGame() {
this.actionToKeys[CustomAction.Next] = new List<Keys>()
{
Keys.Space,
Keys.Enter,
};
}
}
This allows you to write code like entity.Get<KeyboardComponent>().IsKeyDown(CustomAction.Next)
, which would return true
if either the space or enter keys are currently held down.
If your game includes a 2D map on a grid, you can use the Puffin.Core.TileMap
class. An example below creates a new 20x10 map of floor tiles with a border of non-walkable wall tiles.
It assumes your spritesheet has a floor tile then a wall tile (from left to right). Entities need a collision component if they are not to move on solid tiles.
var map = new TileMap(20, 10, "dungeon.png", 32, 32);
map.Define("Floor", 0, 0);
map.Define("Wall", 1, 0, true);
for (var y = 0; y < 10; y++) {
for (var x = 0; x < 20; x++) {
if (x == 0 || y == 0 || x == 19 || y == 9) {
map[x, y] = "Wall";
} else {
map[x, y] = "Floor";
}
}
}
If you simply want collision detection (did two things collide? Are they overlapping?) You can add an OverlapComponent
to your entity (.Overlap(...)
). This allows you to specify functions when another entity with an OverlapComponent
overlaps yours.
var coin = new Entity().Sprite("coin.png").Overlap(40, 40, 0, 0, (e) => {
if (e == player) {
// ... play coin noise ...
// ... increment coins by +1
}
});
The second set of coordinates specify an offset of the overlap, relative to the entity origin. This is useful for things like creating an overlap area larger than an entity's sprite, or noting overlap only if the player walks into the bottom part of a door:
var door = new Entity().Sprite("door.png") // eg. 32x60 image
.Overlap(32, 20, 0, 40); // 32x20 overlap that's at (0, 40)
Puffin provides built-in support for AABB (axis-aligned or non-rotated bounding boxes), including high-speed ones, and is resistent to "tunneling" (high-speed, small objects going through solid walls/etc. because they move so fast).
By default, Puffin checks for entity/tile collisions (with solid tiles) and entity/entity collisions (as long as both have a CollisionComponent
):
var player = new Entity().Colour(32, 32, 0xFFFFFF).FourWayMovement(200).Collide(32, 32, true);
var wall = new Entity().Colour(128, 16, 0x666666).Collide(128, 16).Move(50, 50);
The player will collide with the wall in all directions, as well as any tiles with solid=true
for any TileMap
instances in the current Scene
.
The third parameter, slideOnCollide
, if true, makes the entity slide along the object in the non-colliding direction, rather than stopping abruptly. It is useful for things like smooth character/NPC movement around solid objects.
I ran a simple MonoGame project and an analogous Puffin project, where I render as many copies of a sprite as possible until the FPS drops from 60 to 50.
Both MonoGame and Puffin exhibit similar performance characteristics; on my test machine, MonoGame reached around 2200 sprites, while Puffin reached around 1900 sprites.
To create a self-contained zip (including .NET Core), you can publish your app via the usual dotnet publish
command. For example, to make a Linux build, run dotnet publish -c Release -r linux-x64 -o publish
.
Make sure you copy all your content (sprites, sound effects, etc.) into the publish
directory.
You can also compress the directory for a smaller file-size.
For a reference Powershell script, see Ali the Android's publish.ps1 script. We intend to provide a similar script in the future.
- To build
Puffin
, you need to installMonoGame
. - To build documentation, download
docfx
. Run it by enteringPuffin.Docs
and runningdocfx docfx.json --serve
.