Featured image of post 【C++ Game Dev from Scratch】Fundamental

【C++ Game Dev from Scratch】Fundamental

Circle following the mouse, the implementation of Tic-Tac-Toe. Notes for fundamental part of this tutorial series

Table of Contents

Overview

Tech Stack:C++ + EasyX

Project Goal: Set up the EasyX environment and complete two small demos (a circle that follows the mouse, and a tic-tac-toe game). Understand the basic structure of a game loop.

Course SourceBilibili-Voidmatrix

Environment Setup

EasyX: Just search for “EasyX” and download it directly from the official site.

To use EasyX functions, include <graphics.h> in your header file.

Demo1 - Circle Follows Mouse

Design Approach

  • Create a window and set up the main game loop
  • Draw a circle that follows the mouse
  • Optimize rendering using double buffering

Development Steps

Initialize Window and Main Loop

Use initgraph() to initialize the window, and a while (true) loop to prevent it from closing instantly:

1
2
3
4
5
6
7
8
9
int main()
{
    initgraph(1280, 720);
    while (true)
    {
        
    }
    return 0;
}

This infinite loop is the basic framework for all games. Input handling and screen updates happen inside it:

1
2
3
4
5
while (true)
{
    // Handle player input
    // Update screen
}

Draw the Circle

Use solidcircle() to draw the circle.

Handle Input

Use peekmessage() to process input.

In EasyX, mouse movement, clicks, and keyboard input are all considered “messages.” These messages are stored in a queue. Each time peekmessage() is called, EasyX tries to pull one message from the queue. If successful, it returns true; otherwise, false. So we use another loop to keep pulling messages until the queue is empty.

According to the docs, peekmessage() requires a parameter msg, which is a pointer to an ExMessage struct. One of its members, message, indicates the type of input (mouse, keyboard, etc.). So the input handling looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
while (true)
{
    ExMessage msg; 
    while (peekmessage(&msg))
    {
        if (msg == WM_MOUSEMOVE)
        {
            // Handle mouse movement
        }
    }
}

Clear Screen

If you don’t clear the screen, the ball will leave a trail as it follows the mouse. Use cleardevice() before each draw.

Optimize Drawing with Double Buffering

Use BeginBatchDraw(), FlushBatchDraw(), and EndBatchDraw():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
BeginBatchDraw();

while (true) // Main game loop
{
    // Handle input
    // Update game state
    
    cleardevice(); // Clear screen
    // Draw
    FlushBatchDraw();   
}

EndBatchDraw();

Key Functions

  • initgraph() – initialize graphics window
  • peekmessage() – get mouse movement messages
  • cleardevice() – clear screen
  • solidcircle(x, y, r) – draw circle
  • BeginBatchDraw(), FlushBatchDraw(), EndBatchDraw() – double buffering

Concepts

EasyX Coordinate System

Origin is at the top-left of the screen. X increases to the right, Y increases downward.

坐标系

Render Buffer

Think of the render buffer as a giant canvas. Drawing functions paint on it. Earlier drawings can be covered by later ones. cleardevice() fills the canvas with the current background color (default is black).

BeginBatchDraw() creates a new invisible canvas. All drawing happens on this canvas until FlushBatchDraw() and EndBatchDraw() swap it with the visible one. This prevents flickering caused by frequent redraws.

Game Loop

A typical game loop repeatedly performs:

1
2
3
4
5
6
while (true)
{
    // Read input
    // Process data    
    // Render screen
}

Initialize game data before the loop (before BeginBatchDraw()), and release resources after the loop ends.

Demo2 - Tic-Tac-Toe Game

Game Description

Players take turns placing X or O on a 3 * 3 grid. If one player gets three in a row (horizontal, vertical, or diagonal), they win. If all nine cells are filled with no winner, it’s a draw. This demo supports local two-player mode only.

Design Approach

Three Core Elements in the Game Loop

Input: Handle mouse left-clicks. If a blank cell is clicked, place a piece.

Data Processing: Check for game-over conditions: three matching pieces in a line or full board. If the game ends, show a popup and exit the loop.

Rendering: Use line() to draw the grid and X pieces (diagonal lines), and circle() for O pieces. Display the current piece type in the top-left corner.

Data Structures

Board and Pieces: Use a 2D array char board_data[3][3] for the board. Use 'X' and 'O' for pieces, and '-' for empty cells.

Game Over Conditions

Win: Check all 8 possible winning combinations for both X and O.

Draw: If no '-' remains and no winner, it’s a draw.

Development Steps

Top-Down Approach

Start with the framework, then fill in details.

Game Loop Skeleton

Use bool running to control the loop. Use CheckWin() and CheckDraw() to determine game status.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
bool running = true;
ExMessage msg;

BeginBatchDraw(); 
while (running) // Main game loop
{

    while (peekmessage(&msg)) 
    {
        // Input
    }
    // Data processing
    if (CheckWin('X'))
    {
        // Pop up message and end the game
        MessageBox(GetHWnd(), _T("X Player wins"), _T("Game over"), MB_OK);
        running = false;
    }
    else if (CheckWin('O'))
    {
        // Similar logic to the above
    }
    else if (CheckDraw())
    {
        // Similar logic to the above
    }
    
    cleardevice(); 
    // Draw all the objects
    DrawBoard(); 
    DrawPiece(); 
    DrawPrompt(); 
    
    FlushBatchDraw(); 
}
EndBatchDraw(); 

Input Logic

Mouse coordinates are in pixels. Convert them to grid indices:

1
2
3
4
5
6
int x = msg.x; // Pixel indices of mouse
int y = msg.y;

int index_x = x / 200; // Grid indices of mouse
int index_y = y / 200;
   

Then place the piece and switch to the other type.

Data Processing Logic

CheckWin() uses brute-force to check 8 patterns.

CheckDraw() loops through all cells to check for '-'.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for (int col = 0; col < 3; col++)
{
    for (int row = 0; row < 3; row++)
    {
        if (board[row][col] == '-')
        {
            
        }
    }
}

Rendering Logic

Board: Use line() with pixel coordinates.

X Pieces: Use diagonal line() calls.

O Pieces: Use circle() with center offset by +100 pixels.

Drawing the prompt message: To make it work in more general coding environments, used some less common types and functions – but they work similarly to C’s printf().

1
2
static TCHAR str[64];
_stprintf_s(str, _T("Current piece type:%c"), current_piece);

Some font styling functions

1
2
settextcolor(RGB(225, 175, 45)); // Set the text color to orange for better visual 									 //	emphasis;
outtextxy(0, 0, str); // Display the string at a specified position

Optimization

Last Piece Not Drawn: If win-check happens before drawing, the popup blocks rendering. So draw first, then check.

High CPU Usage: When a computer runs a while loop, it executes extremely fast—our main game loop can complete thousands of iterations in an instant, consuming a large amount of CPU time. For most displays with a physical refresh rate of only 60Hz, this leads to unnecessary performance waste. A quick and crude solution is to use sleep(15) to force the program to pause for 15 milliseconds after each loop. However, this isn’t recommended. As the game grows in complexity, the amount of computation per loop can vary, depending on how the operating system allocates CPU resources. This means the actual time spent per loop may differ. So instead, we should calculate how long each frame takes to process and dynamically adjust the sleep time afterward. The recommended approach is to set a fixed frame rate manually. To do this, we use the GetTickCount() function, which returns the number of milliseconds since the program started running.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
while (running)
{
    DWORD start_time = GetTickCount(); // Get the start time of the current loop
    
    // Read input
    // Process data    
    // Render screen
  
	DWORD end_time = GetTickCount(); // Get the end time of the current loop
    
	DWORD delta_time = end_time - start_time; // Calculate the time interval
    // Dynamically assign sleep time based on the interval
    // Refresh the screen at 60 frames per second
	if (delta_time < 1000 / 60) // If the interval is less than the time for one frame, 								// sleep; Otherwise, no need to sleep
	{
		Sleep(1000 / 60 - delta_time);
	}
}
// Release resources
}

Full source code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#include <graphics.h>

char board_data[3][3] =
{
	{'-', '-', '-'},
	{'-', '-', '-'},
	{'-', '-', '-'}
};

char current_piece = 'O';

bool CheckWin(char c);
bool CheckDraw();
void DrawBoard();
void DrawPiece();
void DrawPrompt();

int main()
{
    //======= Initialization =======
	initgraph(600, 600);

	ExMessage msg;
	bool running = true;
		
	// Double buffering to prevent screen flickering
	BeginBatchDraw();
	
    //======= Main game loop =======
	while (running)
	{
		DWORD start_time = GetTickCount();

		while (peekmessage(&msg))
		{
            //======= Handle input =======
			// Detect mouse left-click messages
			if (msg.message == WM_LBUTTONDOWN)
			{
				// Calculate click position
				int x = msg.x;
				int y = msg.y;
				
				int index_x = y / 200;
				int index_y = x / 200;

                //========= Handle data processing =========
				// Place piece
				if (board_data[index_y][index_x] == '-')
				{
					board_data[index_y][index_x] = current_piece;	

					// Switch piece type
					if (current_piece == 'O')
					{
						current_piece = 'X';
					}
					else if (current_piece == 'X')
					{
						current_piece = 'O';
					}
				}
			}
		}		
		
		cleardevice();

		//===== Handle rendering =====
		DrawBoard();		
		DrawPiece();
		DrawPrompt();

		FlushBatchDraw();
		
        // Check for win condition -- placed after rendering to ensure the last piece 		  // is drawn
		if (CheckWin('X'))
		{
			MessageBox(GetHWnd(), _T("X player wins"), _T("Game over"), MB_OK);
			running = false;
		}
		else if (CheckWin('O'))
		{
			MessageBox(GetHWnd(), _T("O player wins"), _T("Game over"), MB_OK);
			running = false;
		}
		else if (CheckDraw())
		{
			MessageBox(GetHWnd(), _T("Draw"), _T("Game over"), MB_OK);
			running = false;
		}

        //======= Set frame rate(Optimization)=======
		DWORD end_time = GetTickCount();
		DWORD delta_time = end_time - start_time;
		if (delta_time < 1000 / 60)
		{
			Sleep(1000 / 60 - delta_time); // Optimize performance by capping frame 										   // rate at 60 FPS -- avoid running too fast
		}
	}
	EndBatchDraw();
	return 0;
}

bool CheckWin(char c)
{
	if (board_data[0][0] == c && board_data[0][1] == c && board_data[0][2] == c)
		return true;
	if (board_data[1][0] == c && board_data[1][1] == c && board_data[1][2] == c)
		return true;
	if (board_data[2][0] == c && board_data[2][1] == c && board_data[2][2] == c)
		return true;
	if (board_data[0][0] == c && board_data[1][0] == c && board_data[2][0] == c)
		return true;
	if (board_data[0][1] == c && board_data[1][1] == c && board_data[2][1] == c)
		return true;
	if (board_data[0][2] == c && board_data[1][2] == c && board_data[2][2] == c)
		return true;
	if (board_data[2][0] == c && board_data[1][1] == c && board_data[0][2] == c)
		return true;
	if (board_data[0][0] == c && board_data[1][1] == c && board_data[2][2] == c)
		return true;

	return false;
}

bool CheckDraw()
{
	for (int col = 0; col < 3; col++)
	{
		for (int row = 0; row < 3; row++)
		{
			if (board_data[row][col] == '-')
			{
				return false;
			}
		}
	}
	return true;
}

void DrawBoard()
{
	line(0, 200, 600, 200);
	line(0, 400, 600, 400);
	line(200, 0, 200, 600);
	line(400, 0, 400, 600);
}

void DrawPiece()
{
	for (int col = 0; col < 3; col++) 
	{
		for (int row = 0; row < 3; row++) 
		{
			switch (board_data[row][col])
			{
			case '-':
				break;
			case 'O':
				circle(200 * row + 100, 200 * col + 100, 100);
				break;
			case 'X':
				line(200 * row, 200 * col, 200 * (row + 1), 200 * (col + 1));
				line(200 * (row + 1), 200 * col, 200 * row, 200 * (col + 1));
			}
		}
	}	
}

void DrawPrompt()
{
	static TCHAR str[64];
	_stprintf_s(str, _T("Current piece type:%c"), current_piece);

	settextcolor(RGB(225, 175, 45));
	outtextxy(0, 0, str);
}

Reflection and Summary

This was my first time truly understanding the game loop, double buffering, coordinate systems, and frame rate control. I’ve used C++ and raylib before, but mostly by copying code without fully grasping it. This time, I followed the tutorial step by step, focusing on fast and simple implementation rather than object-oriented design. I used a top-down approach: build the framework first, then solve each problem one by one.

I plan to finish all of VoidMatrix’s tutorials to improve my coding skills and deepen my understanding of game development. I also want to align my coding style with industry standards.

Licensed under CC BY-NC-SA 4.0
Last updated on Oct 16, 2025 11:10 +0200
发表了12篇文章 · 总计5万0千字
本博客已运行
Built with Hugo
Theme Stack designed by Jimmy