Skip to content

2. Try Samples

Siv3D allows you to develop games and applications with short code. Let's experience some examples.

Easy way to run samples

  • Copy each sample's code and replace your editor's code, then rebuild
  • To copy the source code in the article to the clipboard, click the in the upper right corner of the code

2.1 Breakout

  • A sample of breakout game

Code
# include <Siv3D.hpp>

void Main()
{
	// Size of one block
	constexpr Size BrickSize{ 40, 20 };

	// Ball speed (pixels / second)
	constexpr double BallSpeedPerSec = 480.0;

	// Ball velocity
	Vec2 ballVelocity{ 0, -BallSpeedPerSec };

	// Ball
	Circle ball{ 400, 400, 8 };

	// Array of blocks
	Array<Rect> bricks;

	for (int32 y = 0; y < 5; ++y)
	{
		for (int32 x = 0; x < (Scene::Width() / BrickSize.x); ++x)
		{
			bricks << Rect{ (x * BrickSize.x), (60 + y * BrickSize.y), BrickSize };
		}
	}

	while (System::Update())
	{
		// Paddle
		const Rect paddle{ Arg::center(Cursor::Pos().x, 500), 60, 10 };

		// Move the ball
		ball.moveBy(ballVelocity * Scene::DeltaTime());

		// Check blocks in order
		for (auto it = bricks.begin(); it != bricks.end(); ++it)
		{
			// If the block and ball intersect
			if (it->intersects(ball))
			{
				// If intersecting with the top or bottom edge of the block
				if (it->bottom().intersects(ball) || it->top().intersects(ball))
				{
					// Reverse the sign of the Y component of the ball's velocity
					ballVelocity.y *= -1;
				}
				else // If intersecting with the left or right edge of the block
				{
					// Reverse the sign of the X component of the ball's velocity
					ballVelocity.x *= -1;
				}

				// Remove the block from the array (iterator becomes invalid)
				bricks.erase(it);

				// Don't check further
				break;
			}
		}

		// If hitting the ceiling
		if ((ball.y < 0) && (ballVelocity.y < 0))
		{
			// Reverse the sign of the Y component of the ball's velocity
			ballVelocity.y *= -1;
		}

		// If hitting the left or right walls
		if (((ball.x < 0) && (ballVelocity.x < 0))
			|| ((Scene::Width() < ball.x) && (0 < ballVelocity.x)))
		{
			// Reverse the sign of the X component of the ball's velocity
			ballVelocity.x *= -1;
		}

		// If hitting the paddle
		if ((0 < ballVelocity.y) && paddle.intersects(ball))
		{
			// Change the bounce direction (velocity vector) according to the distance from the center of the paddle
			ballVelocity = Vec2{ (ball.x - paddle.center().x) * 10, -ballVelocity.y }.withLength(BallSpeedPerSec);
		}

		// Draw all blocks
		for (const auto& brick : bricks)
		{
			// Change color according to the Y coordinate of the block
			brick.stretched(-1).draw(HSV{ brick.y - 40 });
		}

		// Hide the mouse cursor
		Cursor::RequestStyle(CursorStyle::Hidden);

		// Draw the ball
		ball.draw();

		// Draw the paddle
		paddle.rounded(3).draw();
	}
}
  • When rewritten more compactly, it becomes as follows
  • Among various game engines and game frameworks, only Siv3D can describe breakout in this short length (25 LoC)
Compact code
# include <Siv3D.hpp>

void Main()
{
    Vec2 velocity{ 0, -480 };

    Circle ball{ 400, 400, 8 };

    Array<Rect> bricks;

    for (auto p : step(Size{ 20, 5 }))
    {
        bricks << Rect{ (p.x * 40), (60 + p.y * 20), 40, 20 };
    }

    while (System::Update())
    {
        const Rect paddle{ Arg::center(Cursor::Pos().x, 500), 60, 10 };

        ball.moveBy(velocity * Scene::DeltaTime());

        for (auto it = bricks.begin(); it != bricks.end(); ++it)
        {
            if (it->intersects(ball))
            {
                ((it->bottom().intersects(ball) || it->top().intersects(ball)) ? velocity.y : velocity.x) *= -1;
                bricks.erase(it);
                break;
            }
        }

        if ((ball.y < 0) && (velocity.y < 0))
        {
            velocity.y *= -1;
        }

        if (((ball.x < 0) && (velocity.x < 0)) || ((Scene::Width() < ball.x) && (0 < velocity.x)))
        {
            velocity.x *= -1;
        }

        if ((0 < velocity.y) && paddle.intersects(ball))
        {
            velocity = Vec2{ (ball.x - paddle.center().x) * 10, -velocity.y }.withLength(480);
        }

        for (const auto& brick : bricks)
        {
            brick.stretched(-1).draw(HSV{ brick.y - 40 });
        }

        ball.draw();

        paddle.rounded(3).draw();
    }
}

2.2 Typing Game

  • The basic functions of a typing game can be implemented as follows
  • The characteristic is that what you want to do (text input update, drawing, state update) is directly and intuitively reflected in the code

Code
# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// List of problem sentences
	const Array<String> texts =
	{
		U"Practice makes perfect.",
		U"Don't cry over spilt milk.",
		U"Faith will move mountains.",
		U"Nothing ventured, nothing gained.",
		U"Bad news travels fast.",
	};

	// Randomly select a problem sentence
	String target = texts.choice();

	// String being entered
	String input;

	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	while (System::Update())
	{
		// Text input (TextInputMode::DenyControl: do not accept enter, tab, backspace)
		TextInput::UpdateText(input, TextInputMode::DenyControl);

		// If incorrect input is included, delete it
		while (not target.starts_with(input))
		{
			input.pop_back();
		}

		// If matched, move to the next problem
		if (input == target)
		{
			// Randomly select a problem sentence
			target = texts.choice();

			// Clear the input string	
			input.clear();
		}

		// Draw the problem sentence
		font(target).draw(40, Vec2{ 40, 80 }, ColorF{ 0.98 });

		// Draw the characters being entered
		font(input).draw(40, Vec2{ 40, 80 }, ColorF{ 0.12 });
	}
}

2.3 Emoji Tower

  • Drop emojis by clicking to create a tower that physically stacks up
  • A series of functions (polygon generation from images, physics simulation, camera operation, drawing) are completed with compact code

Code
# include <Siv3D.hpp>

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.2, 0.7, 1.0 });

	// Emojis that appear
	const Array<String> emojis = { U"🐘", U"🐧", U"🐐", U"🐤" };

	Array<MultiPolygon> polygons;

	Array<Texture> textures;

	for (const auto& emoji : emojis)
	{
		// Create shape information from emoji image
		polygons << Emoji::CreateImage(emoji).alphaToPolygonsCentered().simplified(2.0);

		// Create texture from emoji image
		textures << Texture{ Emoji{ emoji } };
	}

	// 2D physics simulation step (seconds)
	constexpr double StepTime = (1.0 / 200.0);

	// 2D physics simulation accumulated time (seconds)
	double accumulatedTime = 0.0;

	// 2D physics world
	P2World world;

	// [_] Ground
	const P2Body ground = world.createLine(P2Static, Vec2{ 0, 0 }, Line{ -300, 0, 300, 0 });

	// Animal bodies
	Array<P2Body> bodies;

	// Correspondence table between body ID and emoji index
	HashTable<P2BodyID, size_t> table;

	// Emoji index
	size_t index = Random(polygons.size() - 1);

	// 2D camera
	Camera2D camera{ Vec2{ 0, -200 } };

	while (System::Update())
	{
		accumulatedTime += Scene::DeltaTime();

		while (StepTime <= accumulatedTime)
		{
			// Update the 2D physics world
			world.update(StepTime);

			accumulatedTime -= StepTime;
		}

		// Remove bodies that fell below the ground
		for (auto it = bodies.begin(); it != bodies.end();)
		{
			if (100 < it->getPos().y)
			{
				// Also remove from correspondence table
				table.erase(it->id());

				it = bodies.erase(it);
			}
			else
			{
				++it;
			}
		}

		// Update the 2D camera
		camera.update();
		{
			// Create Transformer2D from 2D camera
			const auto t = camera.createTransformer();

			// If left-clicked
			if (MouseL.down())
			{
				// Add a body
				bodies << world.createPolygons(P2Dynamic, Cursor::PosF(), polygons[index], P2Material{ 0.1, 0.0, 1.0 });

				// Add the pair of body ID and emoji index to the correspondence table
				table.emplace(bodies.back().id(), std::exchange(index, Random(polygons.size() - 1)));
			}

			// Draw all bodies
			for (const auto& body : bodies)
			{
				textures[table[body.id()]].rotated(body.getAngle()).drawAt(body.getPos());
			}

			// Draw the ground
			ground.draw(Palette::Green);

			// Draw the currently controllable emoji
			textures[index].drawAt(Cursor::PosF(), ColorF{ 1.0, (0.5 + Periodic::Sine0_1(1s) * 0.5) });
		}

		// Draw 2D camera controls
		camera.draw(Palette::Orange);
	}
}

2.4 Slot Machine

  • A sample slot game operated with Space
  • Sound effects are generated programmatically using the built-in soundfont
  • By modifying the highlighted parts of the code, you can change settings such as symbols, appearance probability, and prize money

Code
# include <Siv3D.hpp>

/// @brief Slot game symbol
struct Symbol
{
	/// @brief Symbol
	Texture symbol;

	/// @brief Prize money
	int32 score;
};

void Main()
{
	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Font
	const Font font{ FontMethod::MSDF, 48,
		U"example/font/RocknRoll/RocknRollOne-Regular.ttf" };

	// Game start sound effect
	const Audio soundStart{ Wave{ GMInstrument::Agogo,
		PianoKey::A3, 0.3s, 0.2s } };

	// Reel stop sound effect
	const Audio soundStop{ Wave{ GMInstrument::SteelDrums,
		PianoKey::A3, 0.3s, 0.2s } };

	// Prize winning sound effect (loop playback)
	const Audio soundGet{ Wave{ GMInstrument::TinkleBell,
		PianoKey::A6, 0.1s, 0.0s }, Loop::Yes };

	// List of symbols
	const Array<Symbol> symbols
	{
		{ Texture{ U"💎"_emoji }, 1000 },
		{ Texture{ U"7️⃣"_emoji }, 777 },
		{ Texture{ U"💰"_emoji }, 300 },
		{ Texture{ U"🃏"_emoji }, 100 },
		{ Texture{ U"🍇"_emoji }, 30 },
		{ Texture{ U"🍒"_emoji }, 10 },
	};

	// Basic list of symbols prepared for one reel
	const Array<int32> symbolListBase =
		{ 0, 1, 2, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5 };

	// List of symbols prepared for 3 reels (basic list shuffled)
	const std::array<Array<int32>, 3> symbolLists =
	{
		symbolListBase.shuffled(),
		symbolListBase.shuffled(),
		symbolListBase.shuffled()
	};

	// Draw positions of 3 reels
	const std::array<Rect, 3> reels
	{
		Rect{ 80, 100, 130, 300 },
		Rect{ 230, 100, 130, 300 },
		Rect{ 380, 100, 130, 300 },
	};

	// Draw position of money
	const RoundRect moneyRect{ 560, 440, 190, 60, 20 };

	// Rotation amount of 3 reels
	std::array<double, 3> rolls = { 0.0, 0.0, 0.0 };

	// Reel stop count in current game (result judgment at 3 times)
	int32 stopCount = 3;

	// Money possessed
	int32 money = 1000;

	while (System::Update())
	{
		// If space key is pressed
		if (KeySpace.down())
		{
			// If all 3 reels are stopped
			if (stopCount == 3)
			{
				// If money is 3 or more
				if (3 <= money)
				{
					// Reduce money by 3
					money -= 3;

					// Reset reel stop count to 0
					stopCount = 0;

					// Play game start sound effect
					soundStart.playOneShot();
				}
			}
			else
			{
				// Stop the reel at integer position
				rolls[stopCount] = Math::Ceil(rolls[stopCount]);

				// Increase reel stop count
				++stopCount;

				// Play reel stop sound effect
				soundStop.playOneShot();

				// If all 3 reels are stopped
				if (stopCount == 3)
				{
					// Symbols of each reel
					const int32 r0 = symbolLists[0][(
						static_cast<int32>(rolls[0] + 1) % symbolLists[0].size())];
					const int32 r1 = symbolLists[1][(
						static_cast<int32>(rolls[1] + 1) % symbolLists[1].size())];
					const int32 r2 = symbolLists[2][(
						static_cast<int32>(rolls[2] + 1) % symbolLists[2].size())];

					// If all 3 reel symbols are the same
					if ((r0 == r1) && (r1 == r2))
					{
						// Add prize money to money
						money += symbols[r0].score;

						// Play prize winning sound effect
						soundGet.play();

						// Stop prize winning sound effect after 1.5 seconds
						soundGet.stop(1.5s);
					}
				}
			}
		}

		// Reel rotation
		for (int32 i = 0; i < 3; ++i)
		{
			// Skip stopped reels
			if (i < stopCount)
			{
				continue;
			}

			// Increase reel rotation amount according to elapsed time from previous frame
			rolls[i] += (Scene::DeltaTime() * 12);
		}

		// Draw reels
		for (int32 k = 0; k < 3; ++k)
		{
			// Reel background
			reels[k].draw();

			// Draw reel symbols
			for (int32 i = 0; i < 4; ++i)
			{
				// Which element of the reel to point to (integer part of rotation amount)
				const int32 index = (static_cast<int32>(rolls[k] + i)
					% symbolLists[k].size());

				// Symbol index
				const int32 symbolIndex = symbolLists[k][index];

				// Symbol position correction (fractional part of rotation amount)
				const double t = Math::Fraction(rolls[k]);

				// Draw symbol
				symbols[symbolIndex].symbol.resized(90)
					.drawAt(reels[k].center().movedBy(0, 140 * (1 - i + t)));
			}
		}

		// Draw background color above and below reels to hide overflowing symbols
		Rect{ 80, 0, 430, 100 }.draw(Scene::GetBackground());
		Rect{ 80, 400, 430, 200 }.draw(Scene::GetBackground());

		// Draw reel shadows and frames
		for (const auto& reel : reels)
		{
			// Top shadow
			Rect{ reel.tl(), reel.w, 40 }.draw(Arg::top(0.0, 0.3), Arg::bottom(0.0, 0.0));

			// Bottom shadow
			Rect{ (reel.bl() - Point{ 0, 40 }), reel.w, 40 }.draw(Arg::top(0.0, 0.0), Arg::bottom(0.0, 0.3));

			// Frame
			reel.drawFrame(4, ColorF{ 0.5 });
		}

		// Draw 2 triangles pointing to the center
		Triangle{ 60, 250, 36, 90_deg }.draw(ColorF{ 1.0, 0.2, 0.2 });
		Triangle{ 530, 250, 36, -90_deg }.draw(ColorF{ 1.0, 0.2, 0.2 });

		// Draw symbol list
		RoundRect{ 560, 100, 190, 300, 20 }.draw(ColorF{ 0.9, 0.95, 1.0 });

		for (size_t i = 0; i < symbols.size(); ++i)
		{
			// Draw symbol
			symbols[i].symbol.resized(32).draw(Vec2{ 586, (114 + i * 48) });

			// Draw prize money
			font(symbols[i].score).draw(TextStyle::OutlineShadow(0.2, ColorF{ 0.5, 0.3, 0.2 },
				Vec2{ 1.5, 1.5 }, ColorF{ 0.5, 0.3, 0.2 }),
				25, Arg::topRight(720, (109 + i * 48)), ColorF{ 1.0, 0.9, 0.1 });

			if (i != 0)
			{
				// Draw separator line between symbols
				Rect{ 570, (105 + i * 48), 170, 1 }.draw(ColorF{ 0.7 });
			}
		}

		// Draw money background
		if (soundGet.isPlaying())
		{
			// Blink while winning prize
			const ColorF color = Periodic::Sine0_1(0.3s) * ColorF { 0.5, 0.6, 0.7 };
			moneyRect.draw(color).drawFrame(1);
		}
		else
		{
			moneyRect.draw(ColorF{ 0.1, 0.2, 0.3 }).drawFrame(1);
		}

		// Draw money
		font(money).draw(30, Arg::rightCenter(moneyRect.rightCenter().movedBy(-30, 0)));
	}
}
  • A sample of a "Cookie Clicker" type game where you increase the number of items by clicking and purchasing production facilities
  • Includes save data creation and loading, so you can resume from where you left off even after exiting the game

Code
# include <Siv3D.hpp>

// Game save data
struct SaveData
{
    double cookies;

    Array<int32> itemCounts;

    // Define member function for serialization support
    template <class Archive>
    void SIV3D_SERIALIZE(Archive& archive)
    {
        archive(cookies, itemCounts);
    }
};

/// @brief Item button
/// @param rect Button area
/// @param texture Button emoji
/// @param font Font used for text drawing
/// @param name Item name
/// @param desc Item description
/// @param count Item possession count
/// @param enabled Whether the button can be pressed
/// @return true if button was pressed, false otherwise
bool Button(const Rect& rect, const Texture& texture, const Font& font, const String& name, const String& desc, int32 count, bool enabled)
{
    if (enabled)
    {
        rect.draw(ColorF{ 0.3, 0.5, 0.9, 0.8 });

        rect.drawFrame(2, 2, ColorF{ 0.5, 0.7, 1.0 });

        if (rect.mouseOver())
        {
            Cursor::RequestStyle(CursorStyle::Hand);
        }
    }
    else
    {
        rect.draw(ColorF{ 0.0, 0.4 });

        rect.drawFrame(2, 2, ColorF{ 0.5 });
    }

    texture.scaled(0.5).drawAt(rect.x + 50, rect.y + 50);

    font(name).draw(30, rect.x + 100, rect.y + 15, Palette::White);

    font(desc).draw(18, rect.x + 102, rect.y + 60, Palette::White);

    font(count).draw(50, Arg::rightCenter((rect.x + rect.w - 20), (rect.y + 50)), Palette::White);

    return (enabled && rect.leftClicked());
}

// Cookie falling effect
struct CookieBackgroundEffect : IEffect
{
    // Initial position
    Vec2 m_start;

    // Rotation angle
    double m_angle;

    // Texture
    Texture m_texture;

    CookieBackgroundEffect(const Vec2& start, const Texture& texture)
        : m_start{ start }
        , m_angle{ Random(2_pi) }
        , m_texture{ texture } {
    }

    bool update(double t) override
    {
        const Vec2 pos = m_start + 0.5 * t * t * Vec2{ 0, 120 };

        m_texture.scaled(0.3).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t / 3.0) });

        return (t < 3.0);
    }
};

// Cookie dancing effect
struct CookieEffect : IEffect
{
    // Initial position
    Vec2 m_start;

    // Initial velocity
    Vec2 m_velocity;

    // Scale
    double m_scale;

    // Rotation angle
    double m_angle;

    // Texture
    Texture m_texture;

    CookieEffect(const Vec2& start, const Texture& texture)
        : m_start{ start }
        , m_velocity{ Circular{ 80, Random(-40_deg, 40_deg) } }
        , m_scale{ Random(0.2, 0.3) }
        , m_angle{ Random(2_pi) }
        , m_texture{ texture } {
    }

    bool update(double t) override
    {
        const Vec2 pos = m_start
            + m_velocity * t + 0.5 * t * t * Vec2{ 0, 120 };

        m_texture.scaled(m_scale).rotated(m_angle).drawAt(pos, ColorF{ 1.0, (1.0 - t) });

        return (t < 1.0);
    }
};

// "+1" rising effect
struct PlusOneEffect : IEffect
{
    // Initial position
    Vec2 m_start;

    // Font
    Font m_font;

    PlusOneEffect(const Vec2& start, const Font& font)
        : m_start{ start }
        , m_font{ font } {
    }

    bool update(double t) override
    {
        m_font(U"+1").drawAt(24, m_start.movedBy(0, t * -120), ColorF{ 1.0, (1.0 - t) });

        return (t < 1.0);
    }
};

// Item data
struct Item
{
    // Item emoji
    Texture emoji;

    // Item name
    String name;

    // Cost when purchasing the item for the first time
    int32 initialCost;

    // Item CPS
    int32 cps;

    // Returns the purchase cost when having count items
    int32 getCost(int32 count) const
    {
        return initialCost * (count + 1);
    }
};

// Cookie spring
class CookieSpring
{
public:

    void update(double deltaTime, bool pressed)
    {
        // Add spring accumulated time
        m_accumulatedTime += deltaTime;

        while (0.005 <= m_accumulatedTime)
        {
            // Spring force (direction that cancels change)
            double force = (-0.02 * m_x);

            // Force that works when screen is pressed
            if (pressed)
            {
                force += 0.004;
            }

            // Apply force to velocity (also apply damping)
            m_velocity = (m_velocity + force) * 0.92;

            // Reflect in position
            m_x += m_velocity;

            m_accumulatedTime -= 0.005;
        }
    }

    double get() const
    {
        return m_x;
    }

private:

    // Spring extension
    double m_x = 0.0;

    // Spring velocity
    double m_velocity = 0.0;

    // Spring accumulated time
    double m_accumulatedTime = 0.0;
};

// Function to draw cookie halo
void DrawHalo(const Vec2& center)
{
    for (int32 i = 0; i < 4; ++i)
    {
        double startAngle = Scene::Time() * 15_deg + i * 90_deg;
        Circle{ center, 180 }.drawPie(startAngle, 60_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
    }

    for (int32 i = 0; i < 6; ++i)
    {
        double startAngle = Scene::Time() * -15_deg + i * 60_deg;
        Circle{ center, 180 }.drawPie(startAngle, 40_deg, ColorF{ 1.0, 0.3 }, ColorF{ 1.0, 0.0 });
    }
}

// Function to calculate CPS based on item possession count
int32 CalculateCPS(const Array<Item>& ItemTable, const Array<int32>& itemCounts)
{
    int32 cps = 0;

    for (size_t i = 0; i < ItemTable.size(); ++i)
    {
        cps += ItemTable[i].cps * itemCounts[i];
    }

    return cps;
}

void Main()
{
    // Cookie emoji
    const Texture texture{ U"🍪"_emoji };

    // Item data
    const Array<Item> ItemTable = {
        { Texture{ U"🌾"_emoji }, U"クッキー農場", 10, 1 },
        { Texture{ U"🏭"_emoji }, U"クッキー工場", 100, 10 },
        { Texture{ U"⚓"_emoji }, U"クッキー港", 1000, 100 },
    };

    // Possession count of each item
    Array<int32> itemCounts(ItemTable.size()); // = { 0, 0, 0 }

    // Font
    const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

    // Cookie click circle
    constexpr Circle CookieCircle{ 170, 300, 100 };

    // Effects
    Effect effectBackground, effect;

    // Cookie spring
    CookieSpring cookieSpring;

    // Number of cookies
    double cookies = 0;

    // Game elapsed time accumulation
    double accumulatedTime = 0.0;

    // Background cookie accumulated time
    double cookieBackgroundAccumulatedTime = 0.0;

    // If save data is found, load it
    {
        // Open binary file
        Deserializer<BinaryReader> reader{ U"game.save" };

        if (reader) // If open succeeded
        {
            SaveData saveData;

            reader(saveData);

            cookies = saveData.cookies;

            for (size_t i = 0; i < Min(ItemTable.size(), saveData.itemCounts.size()); ++i)
            {
                itemCounts[i] = saveData.itemCounts[i];
            }
        }
    }

    while (System::Update())
    {
        // Calculate cookie production per second
        const int32 cps = CalculateCPS(ItemTable, itemCounts);

        // Add game elapsed time
        accumulatedTime += Scene::DeltaTime();

        // If accumulated 0.1 seconds or more
        if (0.1 <= accumulatedTime)
        {
            accumulatedTime -= 0.1;

            // Add 0.1 seconds worth of cookie production
            cookies += (cps * 0.1);
        }

        // Background cookies
        {
            // Calculate appropriate interval for background cookies to appear from cps (gradually gets smaller to avoid too many, with lower limit)
            const double cookieBackgroundSpawnTime = cps ? Max(1.0 / Math::Log2(cps * 2), 0.03) : Math::Inf;

            if (cps)
            {
                cookieBackgroundAccumulatedTime += Scene::DeltaTime();
            }

            while (cookieBackgroundSpawnTime <= cookieBackgroundAccumulatedTime)
            {
                effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);

                cookieBackgroundAccumulatedTime -= cookieBackgroundSpawnTime;
            }
        }

        // Update cookie spring
        cookieSpring.update(Scene::DeltaTime(), CookieCircle.leftPressed());

        // If mouse cursor is over cookie circle
        if (CookieCircle.mouseOver())
        {
            Cursor::RequestStyle(CursorStyle::Hand);
        }

        // If cookie circle is left-clicked
        if (CookieCircle.leftClicked())
        {
            ++cookies;

            // Add cookie dancing effect
            effect.add<CookieEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-5, 5)), texture);

            // Add "+1" rising effect
            effect.add<PlusOneEffect>(Cursor::Pos().movedBy(Random(-5, 5), Random(-15, -5)), font);

            // Add background cookie
            effectBackground.add<CookieBackgroundEffect>(RandomVec2(Rect{ 0, -150, 800, 100 }), texture);
        }

        // Draw background
        Rect{ 0, 0, 800, 600 }.draw(Arg::top = ColorF{ 0.6, 0.5, 0.3 }, Arg::bottom = ColorF{ 0.2, 0.5, 0.3 });

        // Draw falling background cookies
        effectBackground.update();

        // Draw cookie halo
        DrawHalo(CookieCircle.center);

        // Display number of cookies as integer
        font(ThousandsSeparate((int32)cookies)).drawAt(60, 170, 100);

        // Display cookie production
        font(U"毎秒: {}"_fmt(cps)).drawAt(24, 170, 160);

        // Draw cookie
        texture.scaled(1.5 - cookieSpring.get()).drawAt(CookieCircle.center);

        // Draw effects
        effect.update();

        for (size_t i = 0; i < ItemTable.size(); ++i)
        {
            // Item possession count
            const int32 itemCount = itemCounts[i];

            // Current price of item
            const int32 itemCost = ItemTable[i].getCost(itemCount);

            // CPS per item
            const int32 itemCps = ItemTable[i].cps;

            // Button
            if (Button(Rect{ 340, (40 + 120 * i), 420, 100 }, ItemTable[i].emoji,
                font, ItemTable[i].name, U"C{} / {} CPS"_fmt(itemCost, itemCps), itemCount, (itemCost <= cookies)))
            {
                cookies -= itemCost;
                ++itemCounts[i];
            }
        }
    }

    // After main loop, save game on exit
    {
        // Open binary file
        Serializer<BinaryWriter> writer{ U"game.save" };

        // Write out data that supports serialization
        writer(SaveData{ cookies, itemCounts });
    }
}

2.6 Minesweeper

  • A sample of minesweeper. Left-click to open cells, right-click to place flags
  • Avoid stepping on bombs while deducing bomb locations and opening cells

Code
# include <Siv3D.hpp>

// Game state
enum class GameState
{
	Game,		// Game in progress
	Failed,		// Game over
	Cleared,	// Game clear
};

// Cell state
struct CellState
{
	// Opened
	bool opened = false;

	// Flagged
	bool flagged = false;

	// Exploded
	bool exploded = false;

	// Island number (used when opening number-less squares at once)
	int32 groupIndex = 0;
};

// Offsets to surrounding squares
constexpr Point Offsets[8] =
{
	{ -1, -1 }, { 0, -1 }, { 1, -1 },
	{ -1,  0 }           , { 1,  0 },
	{ -1,  1 }, { 0,  1 }, { 1,  1 },
};

// Function to return the number of 💣 (-1) around specified square
int32 GetBombCount(const Grid<int32>& grid, const Point& center)
{
	// If self is 💣 (-1), return -1
	if (grid[center] == -1)
	{
		return -1;
	}

	// Number of 💣 (-1) found
	int32 bombCount = 0;

	for (const auto& offset : Offsets)
	{
		// Square to check
		const Point pos = (center + offset);

		// grid.fetch(pos, defaultValue) returns
		// grid[pos] if pos is within range, otherwise returns defaultValue
		if (grid.fetch(pos, 0) == -1) // If 💣 (-1)
		{
			++bombCount;
		}
	}

	return bombCount;
}

// Function to generate board
Grid<int32> MakeGame(const Size& size, int32 bombs)
{
	// Create 2D array for board
	Grid<int32> grid(size);

	// Place specified number of 💣 (-1)
	while (bombs)
	{
		// Random position on 2D array
		const Point pos = RandomPoint((size.x - 1), (size.y - 1));

		// If not yet placed
		if (grid[pos] == 0)
		{
			// Place 💣 (-1)
			grid[pos] = -1;

			// Reduce remaining number of 💣
			--bombs;
		}
	}

	// For all squares
	for (int32 y = 0; y < size.y; ++y)
	{
		for (int32 x = 0; x < size.x; ++x)
		{
			// Calculate numbers. However, 💣 squares remain -1
			grid[y][x] = GetBombCount(grid, Point{ x, y });
		}
	}

	return grid;
}

// Function to create board state
Grid<CellState> MakeStates(const Grid<int32>& grid)
{
	const Size size = grid.size();

	// 2D array same size as board
	Grid<CellState> states(size);

	// Data structure managing connection status of each square
	DisjointSet<int32> ds{ states.num_elements() };

	// For all squares
	for (int32 y = 0; y < size.y; ++y)
	{
		for (int32 x = 0; x < size.x; ++x)
		{
			// Index of own square
			const int32 index = static_cast<int32>(y * size.x + x);

			// If own is 0 square
			if (grid[y][x] == 0)
			{
				// If right square is 0
				if (int nx = (x + 1);
					(nx < size.x) && (grid[y][nx] == 0))
				{
					const int32 east = (index + 1); // Index of right square
					ds.merge(index, east); // Make right square same island
				}

				// If bottom square is 0
				if (int ny = (y + 1);
					(ny < size.y) && (grid[ny][x] == 0))
				{
					const int32 south = (index + size.x); // Index of bottom square
					ds.merge(index, south); // Make bottom square same island
				}
			}
		}
	}

	{
		// Square index
		int32 index = 0;

		// For all squares
		for (int32 y = 0; y < size.y; ++y)
		{
			for (int32 x = 0; x < size.x; ++x)
			{
				// Assign island number
				states[y][x].groupIndex = ds.find(index);

				++index;
			}
		}
	}

	return states;
}

// Function to draw unopened cell blocks
void DrawBlock(const Rect& rect)
{
	Triangle{ rect.tl(), rect.tr(), rect.bl() }.draw(ColorF{ 1.0 });
	Triangle{ rect.tr(), rect.br(), rect.bl() }.draw(ColorF{ 0.5 });
	rect.stretched(-5).draw(ColorF{ 0.75 });
}

// Function to draw board
void DrawGame(const Grid<int32>& grid, const Grid<CellState>& states, const Font& font, const Texture& bombTexture, const Texture& flagTexture, const Point& gamePos, const Size& cellSize)
{
	// Colors for numbers 0-8
	constexpr ColorF NumberColors[9] =
	{
		ColorF{ 0, 0, 0 }, ColorF{ 0, 0, 1 }, ColorF{ 0, 0.5, 0 }, ColorF{ 1, 0, 0 },
		ColorF{ 0, 0, 0.5 }, ColorF{ 0.5, 0, 0 }, ColorF{ 0.5, 0, 0 }, ColorF{ 0.5, 0, 0 }, ColorF{ 0.5, 0, 0 }
	};

	// For all squares
	for (int32 y = 0; y < grid.height(); ++y)
	{
		for (int32 x = 0; x < grid.width(); ++x)
		{
			const auto& state = states[y][x];

			// Top-left coordinate of cell
			const Point pos = (gamePos + (cellSize * Point{ x, y }));

			// Cell area
			const Rect cell{ pos, cellSize };

			if (state.opened) // Opened
			{
				// Draw background
				cell.stretched(-1).draw(ColorF{ 0.75 });

				if (const int32 n = grid[y][x];
					n == -1) // If 💣 (-1) square
				{
					// If explosion spot, make cell red
					if (state.exploded)
					{
						cell.stretched(-1).draw(ColorF{ 1, 0, 0 });
					}

					// Draw bomb
					bombTexture.resized(36).drawAt(cell.center());
				}
				else if (1 <= n) // If number square 1 or more
				{
					// Draw number
					font(n).drawAt(cell.center(), NumberColors[n]);
				}
			}
			else // Not opened
			{
				// Draw block
				DrawBlock(cell);

				// If flagged, draw flag
				if (state.flagged)
				{
					flagTexture.resized(30).drawAt(cell.center());
				}
			}
		}
	}
}

// Function to open squares with specified island number and squares adjacent to them
void OpenGroup(const Grid<int32>& grid, Grid<CellState>& states, const int32 groupIndex)
{
	// For all squares
	for (int32 y = 0; y < grid.height(); ++y)
	{
		for (int32 x = 0; x < grid.width(); ++x)
		{
			auto& state = states[y][x];

			// If square with specified island number
			if (state.groupIndex == groupIndex)
			{
				// Open
				state.opened = true;

				// For surrounding squares
				for (const auto& offset : Offsets)
				{
					const Point neighbor = (Point{ x, y } + offset);

					// If within board range and unopened, open that square also
					if (grid.inBounds(neighbor) && (not states[neighbor].opened))
					{
						states[neighbor].opened = true;

						// If it's a number-less square (0) with different island number, recursively open those too
						if ((grid[neighbor] == 0) && (groupIndex != states[neighbor].groupIndex))
						{
							OpenGroup(grid, states, states[neighbor].groupIndex);
						}
					}
				}
			}
		}
	}
}

// Function to update board
void UpdateGame(GameState& gameState, const Grid<int32>& grid, Grid<CellState>& states, const int32 bombCount, const Point& gamePos, const Size& cellSize)
{
	// Board area
	const Rect gameArea{ gamePos, (grid.size() * cellSize - Point{ 1, 1 }) };

	// Board was left-clicked
	const bool open = gameArea.leftClicked();

	// Board was right-clicked
	const bool flag = gameArea.rightClicked();

	if (open || flag)
	{
		// Position of clicked square
		const Point pos = ((Cursor::Pos() - gamePos) / cellSize);

		if (open && (not states[pos].opened) && (not states[pos].flagged)) // Unopened, unflagged square was left-clicked
		{
			// Open that square
			states[pos].opened = true;

			// If that square is a number-less square (0)
			if (grid[pos] == 0)
			{
				// Also open squares with same island number and squares adjacent to them
				OpenGroup(grid, states, states[pos].groupIndex);
			}

			// If that square is 💣 (-1)
			if (grid[pos] == -1)
			{
				// Game over
				gameState = GameState::Failed;

				// Set exploded flag
				states[pos].exploded = true;

				// Open all 💣 squares
				for (int32 y = 0; y < grid.height(); ++y)
				{
					for (int32 x = 0; x < grid.width(); ++x)
					{
						if (grid[y][x] == -1)
						{
							states[y][x].opened = true;
						}
					}
				}
			}
			else if (states.count_if([](const CellState& c) { return (not c.opened); }) == bombCount)
			{	// If number of unopened squares matches number of bombs
				// Game clear
				gameState = GameState::Cleared;
			}
		}
		else if (flag) // Right-clicked
		{
			// Toggle flag state
			states[pos].flagged = (not states[pos].flagged);
		}
	}
}

void Main()
{
	// Set background color to slightly dark gray
	Scene::SetBackground(ColorF{ 0.5 });

	// Number of squares on board
	constexpr Size GameSize{ 20, 13 };

	// Number of 💣 to place
	constexpr int32 BombCount = 30;

	// Compile error if number of 💣 is 1/4 or more of squares
	static_assert(BombCount < (GameSize.area() / 4));

	// Cell size
	constexpr Size CellSize{ 40, 40 };

	// Board draw position
	constexpr Size GamePos{ 0, 80 };

	// Font for numbers
	const Font font{ FontMethod::MSDF, 32, Typeface::Bold };

	// Bomb emoji
	const Texture bombTexture{ U"💣"_emoji };

	// Flag emoji
	const Texture flagTexture{ U"🚩"_emoji };

	// Face emojis corresponding to GameState
	const std::array<Texture, 3> faceTextures = { Texture{ U"🙂"_emoji }, Texture{ U"😵"_emoji }, Texture{ U"😎"_emoji } };

	// Create board
	Grid<int32> grid = MakeGame(GameSize, BombCount);

	// Create state of each cell
	Grid<CellState> states = MakeStates(grid);

	// Game state
	GameState gameState = GameState::Game;

	// Face button area
	const Rect faceButton{ Arg::center(Scene::Width() / 2, 40), 72 };

	while (System::Update())
	{
		////////////////////////////////
		//
		//	State update
		//
		////////////////////////////////
		{
			// Update board if game in progress
			if (gameState == GameState::Game)
			{
				UpdateGame(gameState, grid, states, BombCount, GamePos, CellSize);
			}

			// Initialize state if face button is pressed
			if (faceButton.leftClicked())
			{
				grid = MakeGame(GameSize, BombCount);
				states = MakeStates(grid);
				gameState = GameState::Game;
			}
		}

		////////////////////////////////
		//
		//	Drawing
		//
		////////////////////////////////
		{
			// Draw board
			DrawGame(grid, states, font, bombTexture, flagTexture, GamePos, CellSize);

			// Draw UI area background
			Rect{ Scene::Width(), 80 }.draw(ColorF{ 0.75 });
			{
				// Draw face button
				DrawBlock(faceButton);

				// Draw face
				faceTextures[FromEnum(gameState)].resized(60).drawAt(faceButton.center());
			}
		}
	}
}

2.7 QR Code Creation

  • A sample that converts text entered in a text box into a QR code and displays it
  • Try reading it with your smartphone camera

Code
# include <Siv3D.hpp>

void Main()
{
	// Change window size
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Text to convert
	TextEditState textEdit{ U"abc" };

	String previous;

	// Dynamic texture for displaying QR code
	DynamicTexture texture;

	while (System::Update())
	{
		// Text input
		SimpleGUI::TextBox(textEdit, Vec2{ 20, 20 }, 1240);

		// If text is updated, recreate QR code
		if (const String current = textEdit.text;
			current != previous)
		{
			// Convert entered text to QR code
			if (const auto qr = QR::EncodeText(current))
			{
				// Update dynamic texture with enlarged image
				texture.fill(QR::MakeImage(qr).scaled(Size{ 500, 500 }, InterpolationAlgorithm::Nearest));
			}

			previous = current;
		}

		// Display QR code
		texture.drawAt(640, 400);
	}
}

2.8 Music Player

  • Plays music files saved on your computer

Code
# include <Siv3D.hpp>

void Main()
{
	// Music
	Audio audio;

	// FFT results
	FFTResult fft;

	// Whether playback position has changed
	bool seeking = false;

	while (System::Update())
	{
		ClearPrint();

		// Playback and performance time
		const String time = (FormatTime(SecondsF{ audio.posSec() }, U"M:ss")
			+ U'/' + FormatTime(SecondsF{ audio.lengthSec() }, U"M:ss"));

		// Progress bar progress
		double progress = static_cast<double>(audio.posSample()) / audio.samples();

		if (audio.isPlaying())
		{
			// FFT analysis
			FFT::Analyze(fft, audio);

			// Visualize results
			for (auto i : step(Min(Scene::Width(), static_cast<int32>(fft.buffer.size()))))
			{
				const double size = Pow(fft.buffer[i], 0.6f) * 1000;
				RectF{ Arg::bottomLeft(i, 480), 1, size }.draw(HSV{ 240.0 - i });
			}

			// Display frequency at mouse cursor position
			Rect{ Cursor::Pos().x, 0, 1, Scene::Height() }.draw();
			Print << U"{:.2f} Hz"_fmt(Cursor::Pos().x * fft.resolution);
		}

		// Play
		if (SimpleGUI::Button(U"Play", Vec2{ 40, 500 }, 120, audio && !audio.isPlaying()))
		{
			// Play with 0.2 second fade-in time
			audio.play(0.2s);
		}

		// Pause
		if (SimpleGUI::Button(U"Pause", Vec2{ 170, 500 }, 120, audio.isPlaying()))
		{
			// Pause with 0.2 second fade-out time
			audio.pause(0.2s);
		}

		// Open music file from folder
		if (SimpleGUI::Button(U"Open", Vec2{ 300, 500 }, 120))
		{
			audio.stop(0.5s);
			audio = Dialog::OpenAudio();
			audio.play();
		}

		// Slider
		if (SimpleGUI::Slider(time, progress, Vec2{ 40, 540 }, 130, 590, (not audio.isEmpty())))
		{
			// Pause with 0.05 second fade-out time
			audio.pause(0.05s);

			// Wait until playback stops
			while (audio.isPlaying())
			{
				System::Sleep(0.01s);
			}

			// Change playback position
			audio.seekSamples(static_cast<size_t>(audio.samples() * progress));

			// Don't resume playback until slider is released to avoid noise
			seeking = true;
		}
		else if (seeking && MouseL.up())
		{
			// Resume playback
			audio.play(0.05s);
			seeking = false;
		}
	}

	// On exit, fade out volume if playing
	if (audio.isPlaying())
	{
		audio.fadeVolume(0.0, 0.3s);
		System::Sleep(0.3s);
	}
}

2.9 Simple 3D Drawing

  • You can perform 3D drawing with short code
Operation Description
W S A D Camera forward/backward/left/right movement
E X Camera up/down movement
Shift or Ctrl pressed with movement keys Change camera movement speed
Up Down Left Right Camera viewpoint movement

Code
# include <Siv3D.hpp>

void Main()
{
	// Resize window and scene to 1280x720
	Window::Resize(1280, 720);

	// Background color (remove sRGB curve with removeSRGBCurve() for linear rendering)
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();

	// UV check texture (using mipmaps. Specify as sRGB texture for correct handling during linear rendering)
	const Texture uvChecker{ U"example/texture/uv.png", TextureDesc::MippedSRGB };

	// Multisample render texture for drawing 3D scenes
	// TextureFormat::R8G8B8A8_Unorm_SRGB for linear color space rendering
	// HasDepth::Yes to use depth buffer for depth comparison
	// resolve() required before using drawing content as it's a multisample render texture
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };

	// Debug camera for 3D scene
	// 30° vertical field of view, camera position (10, 16, -32)
	// Forward/backward: [W][S], left/right: [A][D], up/down: [E][X], viewpoint: arrow keys, acceleration: [Shift][Ctrl]
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	while (System::Update())
	{
		// Update debug camera (camera movement speed: 2.0)
		camera.update(2.0);

		// Set camera to 3D scene
		Graphics3D::SetCameraTransform(camera);

		// 3D drawing
		{
			// Fill renderTexture with background color,
			// set renderTexture as render target for 3D drawing
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

			// Draw floor
			Plane{ 64 }.draw(uvChecker);

			// Draw box
			Box{ -8,2,0,4 }.draw(ColorF{ 0.8, 0.6, 0.4 }.removeSRGBCurve());

			// Draw sphere
			Sphere{ 0,2,0,2 }.draw(ColorF{ 0.4, 0.8, 0.6 }.removeSRGBCurve());

			// Draw cylinder
			Cylinder{ 8, 2, 0, 2, 4 }.draw(ColorF{ 0.6, 0.4, 0.8 }.removeSRGBCurve());
		}

		// Draw 3D scene to 2D scene
		{
			// Execute 3D drawing before resolving renderTexture
			Graphics3D::Flush();

			// Resolve multisample texture
			renderTexture.resolve();

			// Transfer linear rendered renderTexture to scene
			Shader::LinearToScreen(renderTexture);
		}
	}
}

2.10 Terrain Editing

  • You can edit terrain elevation by clicking on the height map in the upper left of the screen

Code
# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);

	// Vertex shader for terrain
	const VertexShader vsTerrain = HLSL{ U"example/shader/hlsl/terrain_forward.hlsl", U"VS" }
		| GLSL{ U"example/shader/glsl/terrain_forward.vert", {{ U"VSPerView", 1 }, { U"VSPerObject", 2 }, { U"VSPerMaterial", 3 }} };

	// Pixel shader for terrain
	const PixelShader psTerrain = HLSL{ U"example/shader/hlsl/terrain_forward.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/terrain_forward.frag", {{ U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 }} };

	// Pixel shader for terrain normal calculation
	const PixelShader psNormal = HLSL{ U"example/shader/hlsl/terrain_normal.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/terrain_normal.frag", {{U"PSConstants2D", 0}} };

	// Exit if shader loading failed
	if ((not vsTerrain) || (not psTerrain) || (not psNormal))
	{
		return;
	}

	// Sky color
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();

	// Textures used for terrain
	const Texture terrainTexture{ U"example/texture/grass.jpg", TextureDesc::MippedSRGB };
	const Texture rockTexture{ U"example/texture/rock.jpg", TextureDesc::MippedSRGB };

	// Brush texture
	const Texture brushTexture{ U"example/particle.png" };

	// Render texture for 3D scene
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };

	// Terrain mesh
	const Mesh gridMesh{ MeshData::Grid({ 128, 128 }, 128, 128) };

	// Debug camera
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	// Height map and normal map
	RenderTexture heightmap{ Size{ 256, 256 }, ColorF{ 0.0 }, TextureFormat::R32_Float };
	RenderTexture normalmap{ Size{ 256, 256 }, ColorF{ 0.0, 0.0, 0.0 }, TextureFormat::R16G16_Float };

	while (System::Update())
	{
		camera.update(2.0);

		// 3D
		{
			Graphics3D::SetCameraTransform(camera);

			const ScopedCustomShader3D shader{ vsTerrain, psTerrain };
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			const ScopedRenderStates3D ss{ { ShaderStage::Vertex, 0, SamplerState::ClampLinear} };
			Graphics3D::SetVSTexture(0, heightmap);
			Graphics3D::SetPSTexture(1, normalmap);
			Graphics3D::SetPSTexture(2, rockTexture);

			gridMesh.draw(terrainTexture);
		}

		// Draw RenderTexture to 2D scene
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}

		if (const bool gen = SimpleGUI::Button(U"Random", Vec2{ 270, 10 });
			(gen || (MouseL | MouseR).pressed())) // Edit terrain
		{
			// Edit height map
			if (gen)
			{
				const PerlinNoiseF perlin{ RandomUint64() };
				Grid<float> grid(256, 256);
				for (auto p : step(grid.size()))
				{
					grid[p] = (perlin.octave2D0_1(p / 256.0f, 5) * 16.0f);
				}
				const RenderTexture noise{ grid };
				const ScopedRenderTarget2D target{ heightmap };
				noise.draw();
			}
			else
			{
				const ScopedRenderTarget2D target{ heightmap };
				const ScopedRenderStates2D blend{ BlendState::Additive };
				brushTexture.scaled(1.0 + MouseL.pressed()).drawAt(Cursor::PosF(), ColorF{ Scene::DeltaTime() * 15.0 });
			}

			// Update normal map
			{
				const ScopedRenderTarget2D target{ normalmap };
				const ScopedCustomShader2D shader{ psNormal };
				const ScopedRenderStates2D blend{ BlendState::Opaque, SamplerState::ClampLinear };
				heightmap.draw();
			}
		}

		// Display height map and normal map on the left
		heightmap.draw(ColorF{ 0.1 });
		normalmap.draw(0, 260);
	}
}

Review Checklist

  • Tried using Siv3D's basic functions such as drawing, keyboard input, mouse input, physics simulation, music playback, and 3D drawing