Tutorial 06: Drawing Your First 2D Shape
What you’ll learn
- What
SpriteBatchdoes and theBegin/Draw/Endorder — and why you never callPresentyourself. - How
GraphicsDeviceManagerandGraphicsDevicerelate to each other. - The 1×1 white texture trick for drawing solid rectangles with no art assets.
Before you start — Tutorial 04: The Game Class Lifecycle (you need Draw to be running) and Tutorial 03: Your First CNA Window (for a window to draw into).
SpriteBatch Introduction
SpriteBatch is CNA's primary 2D drawing interface. It batches multiple sprite draw calls into as few GPU draw calls as possible, making it efficient for drawing many sprites per frame.
The workflow for every 2D rendering frame is:
- Clear the back buffer with
device.Clear(color) - Call
spriteBatch_->Begin() - Issue as many
spriteBatch_->Draw(...)calls as needed - Call
spriteBatch_->End()— this flushes the batch to the GPU - Return from
Draw()— theGamethen presents (swaps) the back buffer for you
CNA's SpriteBatch matches the XNA 4.0 API. If you have used MonoGame or XNA before, this will feel familiar.
GraphicsDevice and GraphicsDeviceManager
GraphicsDeviceManager is a component you create in the constructor. It creates and manages the GraphicsDevice for the game window and presents each finished frame. You access the graphics device via getGraphicsDeviceProperty():
// In Draw():
auto& device = getGraphicsDeviceProperty(); // returns GraphicsDevice&
device.Clear(Color::CornflowerBlue); // clear to a color
// No device.Present() here: Game::EndDraw() swaps the back buffer
// automatically after Draw() returns.
The GraphicsDevice also gives you the current viewport dimensions, which you will use to center objects on screen:
auto& vp = getGraphicsDeviceProperty().getViewportProperty();
int screenWidth = vp.getWidthProperty(); // e.g. 800
int screenHeight = vp.getHeightProperty(); // e.g. 600
The 1×1 White Texture Trick
XNA does not have a primitive rectangle drawing function. The standard approach is to create a 1×1 white Texture2D and stretch it to any size. Multiplying by a color tints the white pixels to that color.
// Create a 1x1 white texture
pixel_ = std::make_unique<Texture2D>(getGraphicsDeviceProperty(), 1, 1);
Color white = Color::White; // RGBA (255,255,255,255)
pixel_->SetData(&white, 1); // upload one pixel to the GPU
This is the exact same technique used in XNA and MonoGame games. The texture costs essentially nothing: 4 bytes of VRAM.
Drawing Colored Rectangles
Once you have the 1×1 pixel texture, draw a filled rectangle by passing a Rectangle as the destination:
spriteBatch_->Begin();
// Draw a red rectangle at (50, 50), 200 pixels wide, 100 pixels tall
spriteBatch_->Draw(
*pixel_, // Texture2D&
Rectangle(50, 50, 200, 100), // destination rectangle
Color::Red // tint color
);
// Draw a semi-transparent blue rectangle
spriteBatch_->Draw(
*pixel_,
Rectangle(100, 200, 150, 80),
Color::Blue * 0.5f // 50% blue: scales R, G, B *and* A
);
spriteBatch_->End();
Why Color::Blue * 0.5f and not Color(0, 0, 255, 128)? SpriteBatch::Begin() with no arguments uses BlendState::AlphaBlend, which (as in XNA 4.0) expects premultiplied alpha: colour channels already multiplied by alpha. Multiplying a Color by a float scales all four channels, which is exactly that. A tint such as Color(0, 0, 255, 128) keeps full-strength colour with half alpha and renders brighter than you intended (pure black with alpha, like the panel below, is the one case that looks the same either way). Tutorial 07 and Tutorial 22 go deeper.
The Rectangle struct holds X, Y, Width, Height as integers. X and Y are the top-left corner in screen pixels, with (0,0) at the top-left of the window.
SpriteBatch Begin and End
Begin() and End() bracket a batch of draw calls. You must call them in pairs:
// Default Begin — deferred mode (sprites are drawn at End(), in the
// order you submitted them), AlphaBlend, LinearClamp sampling
spriteBatch_->Begin();
// Immediate mode — each Draw flushes immediately (slower, but predictable)
spriteBatch_->Begin(SpriteSortMode::Immediate, BlendState::AlphaBlend);
// Back-to-front depth sorting (uses the layerDepth argument of Draw)
spriteBatch_->Begin(SpriteSortMode::BackToFront, BlendState::AlphaBlend);
// Additive blending — good for particles and lights
spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::Additive);
There is no Begin(SpriteSortMode) on its own: as soon as you name a sort mode you also pass a blend state. For now, the default parameterless Begin() is sufficient. We will cover blend states and sorting in later tutorials (22, 25).
Complete example
Here is a game that draws several colored rectangles demonstrating size, position, and color:
#include <memory>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Rectangle.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;
class ShapeDemo final : public Game {
public:
ShapeDemo() : graphics_(this) {
graphics_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(600);
}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// The 1x1 white pixel — the workhorse of 2D shape drawing
pixel_ = std::make_unique<Texture2D>(getGraphicsDeviceProperty(), 1, 1);
Color white = Color::White;
pixel_->SetData(&white, 1);
}
void Update(GameTime& gameTime) override {
if (Keyboard::GetState().IsKeyDown(Keys::Escape)) Exit();
}
void Draw(const GameTime& gameTime) override {
auto& device = getGraphicsDeviceProperty();
device.Clear(Color::CornflowerBlue);
spriteBatch_->Begin();
// Large dark background panel
spriteBatch_->Draw(*pixel_, Rectangle(50, 50, 700, 500),
Color(0, 0, 0, 180));
// A red square
spriteBatch_->Draw(*pixel_, Rectangle(100, 100, 120, 120), Color::Red);
// A green rectangle
spriteBatch_->Draw(*pixel_, Rectangle(280, 100, 200, 80), Color::Green);
// A blue tall rectangle
spriteBatch_->Draw(*pixel_, Rectangle(540, 100, 80, 200), Color::Blue);
// A yellow wide rectangle
spriteBatch_->Draw(*pixel_, Rectangle(100, 300, 400, 50), Color::Yellow);
// A semi-transparent white rectangle (premultiplied: 40% of white)
spriteBatch_->Draw(*pixel_, Rectangle(200, 380, 200, 100),
Color::White * 0.4f);
// An orange square
spriteBatch_->Draw(*pixel_, Rectangle(550, 380, 100, 100),
Color(255, 140, 0, 255));
spriteBatch_->End();
// Game::EndDraw() presents the frame after Draw() returns.
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> pixel_;
};
int main() {
ShapeDemo game;
game.Run();
return 0;
}
Run this and you will see a window with several colored rectangles layered on a cornflower blue background. The semi-transparent rectangle lets the shapes behind it show through.
In Tutorial 07 we go deeper into the Color struct — predefined colors, RGBA construction, lerping, and alpha blending.