![]() |
VOOZH | about |
dotnet add package BenMakesGames.PlayPlayMini --version 8.2.0.1
NuGet\Install-Package BenMakesGames.PlayPlayMini -Version 8.2.0.1
<PackageReference Include="BenMakesGames.PlayPlayMini" Version="8.2.0.1" />
<PackageVersion Include="BenMakesGames.PlayPlayMini" Version="8.2.0.1" />Directory.Packages.props
<PackageReference Include="BenMakesGames.PlayPlayMini" />Project file
paket add BenMakesGames.PlayPlayMini --version 8.2.0.1
#r "nuget: BenMakesGames.PlayPlayMini, 8.2.0.1"
#:package BenMakesGames.PlayPlayMini@8.2.0.1
#addin nuget:?package=BenMakesGames.PlayPlayMini&version=8.2.0.1Install as a Cake Addin
#tool nuget:?package=BenMakesGames.PlayPlayMini&version=8.2.0.1Install as a Cake Tool
PlayPlayMini is an opinionated framework for making smallish 2D games with MonoGame.
It provides a state engine with lifecycle events, a GraphicsManager that provides methods for easily drawing sprites & fonts with a variety of effects, and dependency injection using Autofac.
If you don't know what all of those things are, don't worry: they're awesome, and this readme will show you how to use them (with code examples!), and explain their benefits.
If you prefer learning purely by example, check out Block-break, a demo game made with PlayPlayMini, EntityFramework, and Serliog that uses fonts, sprite sheets, pictures, and sounds.
π§ Hey, listen! You can support my development of open-source software on Patreon
For information on how to upgrade from one major verison to the next, see .
dotnet new install BenMakesGames.PlayPlayMini.Templatesdotnet new playplaymini.skeleton -n MyGameA "game state" is something like "the title menu", "exploring a town", "lock-picking mini-game", etc.
Game states get user input, update your game data, and draw graphics. In PlayPlayMini, there is always exactly one active game state. It's possible to layer game states, for example to handle nested menus.
If you used one of the PlayPlayMini template projects, check out the Startup class in the GameStates folder. It should look like this:
namespace MyGame.GameStates;
// inheriting game states is a path that leads to madness, so always seal your game states!
public sealed class Startup: GameState
{
private GraphicsManager Graphics { get; }
private GameStateManager GSM { get; }
private MouseManager Mouse { get; }
public Startup(GraphicsManager graphics, GameStateManager gsm, MouseManager mouse)
{
Graphics = graphics;
GSM = gsm;
Mouse = mouse;
Mouse.UseCustomCursor("Cursor", (3, 1));
}
// note: you do NOT need to call the `base.` for lifecycle methods. so save some CPU cycles,
// and don't call them :P
public override void Update(GameTime gameTime)
{
if (Graphics.FullyLoaded)
{
// TODO: go to title menu, once that exists; for now, just jump straight into the game:
GSM.ChangeState<Playing>();
}
}
public override void Draw(GameTime gameTime)
{
// TODO: draw loading screen
Mouse.Draw(this);
}
}
There's a few things to unpack in that example:
First: all PlayPlayMini game states must inherit the GameState class.
There are several lifecycle methods which a game state may optionally override:
Enter(), called once, when the game state is entered into.Input(...), called every frame. This is a good place to capture user input for later processing.FixedUpdate(...), called 60 times per second, regardless of the game's framerate; useful for doing physics-based updates.Update(...) called every frame. Most game logic will probably go here.Draw(...), called every frame. Place drawing logic here.Leave(), called once, when the game state is left.The current game state can be changed using one of the GameStateManager's ChangeState methods. The Startup game state uses this to start the game once all of the graphic assets have been loaded:
GSM.ChangeState<Playing>();
Finally, you may have noticed that the Startup game state has a constructor. You'll never call new Startup(...) anywhere, however! This is because PlayPlayMini uses a dependency injection framework called Autofac under the hood.
If you're not familiar with dependency injection, you may find yourself wanting to new up game states and MouseManagers and things. Stop! Don't do it! That path leads to madness. Read on to learn an easier way to manage a class's dependencies:
Feel free to skip/skim this section if you're like "yes, I know all about IoC/DI. I even know all the acronyms."
Consider this line, from the above code:
public Startup(GraphicsManager graphics, GameStateManager gsm, MouseManager mouse)
Here, rather than the Startup class newing up its own graphics manager, game state manager, or mouse, it asks those things to be given via its constructor. That's the main principle of dependency injection: "inversion of control". Rather than a class creating the things it needs, it gives up control of that task to something else.
You may wonder: "Why bother? I'll just have to provide them all when I call new Startup(...) anyway!"
However, with a dependency injection framework, you will never write new Startup(...)!
So how do the game states get made?
Hold that thought. Let's take a moment to look at some of the advantages of never writing new.
First: as your game states grow in number and complexity, you'll want to give them more
"services" like the GraphicsManager, FrameCounter, and others you make yourself (perhaps a SQLite database connection to save and load the game).
If you were newing up the game states "manually", then every time you added a new service to a constructor,
you'd have to find all the places you made a new one, and give them the new things they need.
new Startup(new MouseManager(...???), new GameStateManager(?!!?!?));
Second: if you new up a game state manually, and it needs a MouseManager, you'd also have to create a new MouseManager() for it... but the MouseManager's constructor also has arguments! How are you supposed to get those?!
A DI framework eliminates all these problems. You register all the classes you'll ever want newd, and it news them up for you whenever one is needed. This allows you to easily add/remove/change a class's dependencies without making any other changes across your codebase. Nice!
Third: Many services, like the MouseManager, you really only need one of, so you can use the same instance over and over. You could write global statics, but if you've done that in a big project before, you know the trouble and performance problems that can lead to. Dependency injection frameworks, like Autofac, can be configured to find existing instances of certain service classes, and use those instead of making a new ones, with very little effort. (No need to write lazy-initalization logic, etc.)
So how do you create a new service without writing new? Search the interwebs if you want more details, but to put it simply: the dependency injection framework just does it for you. When you ask PlayPlayMini to change into the Playing class, Autofac creates it for you. And if Playing asks for a MouseManager, autofac creates that, too (or finds one that already exists).
In order for a dependency injection framework to do this, any classes you want it to create need to be registered with it. In PlayPlayMini, your game states, and many of the services you'll use, are registered automatically. You can also register your own services, if you want - more on this, later!
PictureMeta, ORM entities, and other DTOs should still be newed up manually, and should never ask for a service in their own constructors.static methods will be more-performant, easier to re-use (copy-paste into other projects), and easier to test. (If such static methods later grow to the point that they want services, create a new service that calls the static methods + does the extra service-ful logic.)Program.csIf you used one of the PlayPlayMini template projects, check out the Program class in the project root. Skipping the using statements, it should look like this:
var gsmBuilder = new GameStateManagerBuilder();
gsmBuilder
.SetWindowSize(1920 / 4, 1080 / 4, 2)
.SetInitialGameState<Startup>()
.SetLostFocusGameState<LostFocus>()
// TODO: set a better window title
.SetWindowTitle("MyGame")
// TODO: add any resources needed (refer to PlayPlayMini documentation for more info)
.AddAssets(new IAsset[]
{
new FontMeta("Font", "Graphics/Font", 6, 8),
new PictureMeta("Cursor", "Graphics/Cursor", true),
// new FontMeta(...)
// new PictureMeta(...)
// new SpriteSheetMeta(...)
// new SongMeta(...)
// new SoundEffectMeta(...)
})
// TODO: any additional service registration (refer to PlayPlayMini and/or Autofac documentation for more info)
.AddServices((s, c) => {
})
;
gsmBuilder.Run();
Taking it one step at a time:
var gsmBuilder = new GameStateManagerBuilder();
If you've used ASP.NET Core before, this kind of startup logic should look pretty familiar.
The GameStateManagerBuilder is responsible for getting your game configured and started, including creating the dependency injection service container.
.SetWindowSize(1920 / 4, 1080 / 4, 2)
.SetInitialGameState<Startup>()
Hopefully those are pretty self-explanatory. The final 2 in SetWindowSize indicates that all pixels should actually be drawn as 2x2 pixels. Under the hood, PlayPlayMini upscales your graphics, yielding a chunky pixel look! Set the zoom level to 1 if don't want chunky pixels!
Next:
.SetLostFocusGameState<LostFocus>()
This is optional; it configures a game state to be used when your game loses focus. If you don't want this feature, you can delete this line, and the LostFocus class that comes with the template.
Next up:
.AddAssets(new IAsset[] {
...
})
This method tells the GraphicsManager (and SoundManager) which assets to load, from your Content/Content.mcgb file. Content/Content.mcgb is part of MonoGame's asset "pipeline". PlayPlayMini hides a lot of MonoGame's internals, but the asset pipeline isn't something that can be - or should be - hidden! It's how you tell MonoGame what graphics, sounds, and songs, your game will use.
If you've never used the Content/Content.mgcb file before, check out MonoGame's documentation on the subject:
It's a super-useful tool!
Moving on:
new FontMeta("Font", "Graphics/Font", 6, 8),
new PictureMeta("Cursor", "Graphics/Cursor", true),
FontMeta (along with PictureMeta and SpriteSheetMeta) contains everything the
GraphicsManager needs to load and store graphics.
The first argument is the name/key/ID/whatever-you-wanna-call-it which you're assigning to the
asset. It can be anything, and spaces and other punctuation are allowed
(it's just a string, after all!) This name is what you'll use when referring to the asset, for example when asking the GraphicsManager to draw a picture, or the SoundManager to play a sound.
π§ββοΈ Hey, listen! To prevent mistakes, it can be helpful to use
consts for these names.
The second argument is a path to the image, matching your Content/Content.mgcb file's definition of
the image.
For fonts and sprite sheets, the dimensions of each individual sprite are specified in the third and fourth parameters.
FontMeta also accepts optional horizontalSpacing, verticalSpacing, and firstCharacter arguments. The first two control the gap (in pixels) inserted between glyphs and lines at draw time, and default to 1. firstCharacter is the character that the first cell of the sheet represents β defaults to ' ' (space), which is correct for most Latin font sheets. Set it explicitly when your sheet covers a non-Latin range (CJK, Cyrillic, custom symbols), so glyph lookup lands on the right cell.
For multi-lingual fonts, or fonts whose glyphs do not all share the same cell size, use the multi-sheet FontMeta constructor. Each FontSheetMeta covers a contiguous character range, and at draw time the first sheet whose range contains the glyph wins β so put more-specific ranges before any wider fallback:
new FontMeta("Font", new[] {
new FontSheetMeta("Fonts/Latin", 6, 8), // covers ' ' onward
new FontSheetMeta("Fonts/CJK", 12, 12) { FirstCharacter = 'δΈ' }, // covers a CJK range
}),
PlayPlayMini's text APIs (DrawText, DrawTextWithWordWrap, ComputeDimensionsWithWordWrap, etc.) all accept multi-sheet Fonts. Single-sheet fonts continue to take the fast path.
Finally, an optional boolean specifies whether or not the asset needs to be loaded before the game's startup state is entered. In the example above, this is set for the "Cursor" graphic, so that the mouse cursor can be shown while the rest of the assets are loading.
Unless every single one of your assets are set to load before the game's startup state (which is not recommended!), you'll need to wait for them to load before starting the rest of the game.
The template-provided Startup game state does this by checking the GraphicsManager's FullyLoaded attribute (seen in an earlier code example).
If you also have deferred sound effect or music assets, inject the SoundManager, and check on its FullyLoaded property, as well.
Remember: if you try to use an asset before it's loaded, your application will crash!
A "service" is just any class that's been registered with the DI framework (Autofac, in our case).
PlayPlayMini provides many such services, but you can also make your own.
Suppose you make a CombatManager class to control the logic of a turn-based combat system. If you register it as a service, you can ask for a CombatManager in the constructor of any other service (including game states), and you can ask for other services in the constructor of your CombatManager.
See "Creating Your Own Services" below for more info, as well as tips on how to avoid "circular dependencies" (instances where two services request one another in their constructors!)
The GameStateManager is needed to change the game's state, for example to transition from your title menu to your load screen, etc.
Most of your game states will probably include the GameStateManager as one of their dependencies.
The GraphicsManager has methods for drawing graphics and fonts.
Most of your game states will probably include the GraphicsManager as one of their dependencies.
The SoundManager has methods for playing sounds and looping music.
It uses MonoGame's built-in sound library, which has some limitations, and even some audible bugs on some platforms (such as poor looping of music tracks on Windows).
If/when you get your game to a good state, and you really want to upgrade your game's sound and music, I recommend either:
You can still use MonoGame's Keyboard class directly; the KeyboardManager provides some additional features, like checking whether or not a particular key was JUST pressed (without having to write checks for that yourself).
Whether or not you use this class really depends on the kind of game you're making, and whether or not you want to/need to write your own keyboard controls.
You can use still MonoGame's Mouse class directly; the MouseManager provides some additional features, including a method for drawing a custom cursor, and disabling the mouse when there's keyboard activity.
Whether or not you use this class really depends on the kind of game you're making, and whether or not you want to/need to write your own mouse controls.
The FrameCounter counts FPS, and some other stats. Use it if you want to add an FPS indicator on the screen.
Once you've created your class, there are two ways you can register it with Autofac + PlayPlayMini:
AutoRegister AttributePlayPlayMini provides an attribute called AutoRegister which you can attach to a class to register that class with Autofac. When the game starts up, it searches for all classes using this attribute, and registers them for you! This attribute doesn't provide all of the options available when registering manually, but should suffice for the vast majority of use-cases.
A simple example, registering a singleton service without any interface (the most common case for most PlayPlayMini applications):
[AutoRegister]
sealed class MyService
{
...
}
If needed, you can also register services with a per-dependency lifetime, and/or an interface:
[AutoRegister(Lifetime.PerDependency, InstanceOf = typeof(IMyService))]
sealed class MyService: IMyService
{
...
}
Possible lifetimes are:
Lifetime.Singleton (default)
Lifetime.PerDependency
Lifetime.Singleton.One of the methods you can call on the GameStateManagerBuilder is AddServices. If there isn't already a call to it, add one; it would look something like this:
.AddServices((s, c) => {
s.RegisterType<MyService>();
s.RegisterType<SomeOtherService>();
})
If RegisterType doesn't seem to be available, add using AutoFac; to the top of the file. (Your IDE should be nice and suggest this for you.)
For more info on how to register services, check the Autofac documentation: https://autofac.org
A circular dependency is when two services refer to one another in their constructors. Ex:
[AutoRegister]
sealed class ServiceA
{
public ServiceA(ServiceB b)
{
...
}
}
[AutoRegister]
sealed class ServiceB
{
public ServiceB(ServiceA a)
{
...
}
}
If you write code like this, Autofac will complain (and crash) when it tries to instantiate a service that requests either ServiceA or ServiceB.
This can be resolved in two ways:
ServiceC, that services A and B depend on, instead of depending on one another. Move the method or methods needed into ServiceC, and update your code accordingly.ServiceA or ServiceB as a lazy service. There are a few ways to do this; one is to use this NuGet package: https://github.com/servicetitan/lazy-proxy-autofac
[AutoRegister] for quick registration.There are several interfaces which services can implement, each allowing the service to hook into different processes within PlayPlayMini. A service may implement any combination of these interfaces, or none at all. They are:
IServiceDraw
Draw method, which will be called after the current state's Draw methods have been called.FrameCounter service, which displays the FPS on-screen.IServiceInitialize
Initialize method, which will be called at the beginning of the MonoGame Initialize state, before MonoGame's LoadContent method is called.IServiceInput
Input method, which will be called before the current state's Input event.KeyboardManager and MouseManager.IServiceLoadContent
LoadContent method, and FullyLoaded getter. The LoadContent method is called in MonoGame's LoadContent method. It's up to the service class's author to implement FullyLoaded accurately (probably as public bool FullyLoaded { get; private set; }, setting it to true when LoadContent has completed).GraphicsManager implements this interface; just as your game's startup game service waits on GraphicsManager.FullyLoaded, you should almost certainly check FullyLoaded on all such services before starting the rest of the game.IServiceUpdate
Update method, which will be called before the current state's Update method.Here's how you can draw two states at once, for example, to show a pause screen on top of another game state:
public sealed class PauseScreen: GameState<AbstractGameState>
{
private AbstractGameState PreviousState { get; }
...
public PauseScreen(..., AbstractGameState previousState)
{
...
PreviousState = previousState;
}
public override void Draw(GameTime gameTime)
{
PreviousState.Draw(gameTime);
// rest of drawing logic here, for example:
Graphics.DrawFilledRectangle(0, 0, GraphicsManager.Width, GraphicsManager.Height, new Color(0, 0, 0, 0.8));
Graphics.DrawPicture("Paused", 100, 200);
}
public override void Update(GameTime gameTime)
{
PreviousState.Update(gameTime);
// pause screen's own update logic goes here, if any
}
public override void Input(GameTime gameTime)
{
// press Escape to un-pause
if(Keyboard.PressedKey(Keys.Escape))
{
// we have a reference to a GameState, so we can invoke ChangeState this way:
GSM.ChangeState(PreviousState);
}
}
...
}
The pause screen would then be opened like this:
public sealed class CombatEncounter: GameState
{
...
public override void Input(GameTime gameTime)
{
...
if(KeyboardManager.PressedKey(Keys.Escape))
{
GSM.ChangeState<PauseScreen, AbstractGameState>(this);
}
}
...
}
By the way, this ChangeState method can be used to pass more complex parameters to a game state, too. For example, assuming you have some PauseScreenConfig class, you might write:
GSM.ChangeState<PauseScreen, PauseScreenConfig>(new PauseScreenConfig() {
PreviousState = this,
BackgroundOpacity = 0.5f,
PausePicture = "Pause",
});
The PauseScreen game state would then be written like this:
public sealed class PauseScreen: GameState<PauseScreenConfig>
{
private PauseScreenConfig Config { get; }
private AbstractGameState PreviousState => config.PreviousState; // optional, as convenient
...
public PauseScreen(..., PauseScreenConfig config)
{
...
Config = config;
}
...
}
A game state's lifecycle event methods are called, in this order.
Enter
Input
FixedUpdate
UseFixedTimeStep, there's no difference between using FixedUpdate and Update.Update
gameTime to ensure that you update things at a consistent rate, regardless of the current frame rate.Draw
Leave
Leave is called, the current game state is changed to be the next game state.| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 5 NuGet packages that depend on BenMakesGames.PlayPlayMini:
| Package | Downloads |
|---|---|
|
BenMakesGames.PlayPlayMini.UI
An extension for PlayPlayMini, adding a skinnable, object-oriented UI framework. |
|
|
BenMakesGames.PlayPlayMini.GraphicsExtensions
Some GraphicsManager extensions for PlayPlayMini. |
|
|
BenMakesGames.PlayPlayMini.BeepBoop
An extension for PlayPlayMini which adds methods for generating & playing simple waveforms. |
|
|
BenMakesGames.PlayPlayMini.NAudio
Get seamless looping music, and cross-fade, in your MonoGame-PlayPlayMini game using NAudio. |
|
|
BenMakesGames.PlayPlayMini.VN
Add-on for PlayPlayMini to facilitate adding visual novel segments to your games. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 8.2.0.1 | 92 | 6/14/2026 |
| 8.2.0 | 130 | 6/14/2026 |
| 8.1.0 | 156 | 4/28/2026 |
| 8.0.0 | 152 | 4/16/2026 |
| 8.0.0-rc7 | 156 | 2/9/2026 |
| 7.2.0 | 193 | 1/9/2026 |
| 7.1.0 | 245 | 12/21/2025 |
| 7.0.0 | 368 | 11/13/2025 |
| 7.0.0-rc2 | 118 | 11/1/2025 |
| 7.0.0-rc1 | 137 | 11/1/2025 |
| 6.3.1 | 280 | 10/30/2025 |
| 6.3.0 | 219 | 9/27/2025 |
| 6.3.0-beta1 | 220 | 8/20/2025 |
| 6.2.0 | 282 | 6/6/2025 |
| 6.1.0 | 321 | 6/2/2025 |
| 6.0.0 | 386 | 4/14/2025 |
| 5.6.0 | 287 | 3/31/2025 |
| 5.5.0 | 420 | 3/5/2025 |
| 5.4.1 | 267 | 2/24/2025 |
| 5.3.1 | 252 | 2/24/2025 |