Tutorial 06: Drawing Your First 2D Shape

CNA Tutorial Series  ·  Beginner

ℹ

What you’ll learn

  • What SpriteBatch does and the Begin / Draw / End order — and why you never call Present yourself.
  • How GraphicsDeviceManager and GraphicsDevice relate 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:

  1. Clear the back buffer with device.Clear(color)
  2. Call spriteBatch_->Begin()
  3. Issue as many spriteBatch_->Draw(...) calls as needed
  4. Call spriteBatch_->End() — this flushes the batch to the GPU
  5. Return from Draw() — the Game then 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.