reference resources
- Interesting programming of C and C + + Games by Tong Jing
Stick Run Mobile
The idea of the game is that players control matchmaker's running and jumping through the keyboard to avoid bats and reach the end. The game map is randomly generated. With the increase of the number of levels, the game becomes more and more difficult
Define Player class
class Player { public: IMAGE im_show; // Image to be displayed at the current time float x_left, y_bottom; // Lower left corner position float vx, vy; // speed float width, height; // Width and height of the picture void draw() { putimagePng(x_left, y_bottom - height, &im_show); } void initialize() { loadimage(&im_show, _T("standright.png")); width = im_show.getwidth(); height = im_show.getheight(); updateXY(WIDTH / 2, HEIGHT / 2); vx = 10; vy = 10; } void updateXY(float mx, float my) { x_left = mx; y_bottom = my; } };
Define global variables, initialize and display:
IMAGE im_land; IMAGE im_bk; Player player; void startup() { player.initialize(); loadimage(&im_land, _T("land.png")); loadimage(&im_bk, _T("landscape1.png")); initgraph(WIDTH, HEIGHT); BeginBatchDraw(); } void show() { putimage(-100, -100, &im_bk); putimage(WIDTH / 2, HEIGHT / 2, &im_land); player.draw(); FlushBatchDraw(); }
Control character movement by pressing the key:
void updateWithInput() { if (_kbhit()) { char input = getch(); if (input == 'A') { player.x_left -= player.vx; } if (input == 'D') { player.x_left += player.vx; } if (input == 'W') { player.y_bottom -= player.vy; } if (input == 'S') { player.y_bottom += player.vy; } } }
Asynchronous input
Using the asynchronous input function GetAsyncKeyState(), you can identify the situation when two keys are pressed at the same time. Players can use A, S, D, W or left, right, up and down direction keys to control the movement of matchmaker:
void updateWithInput() { if (_kbhit()) { if (GetAsyncKeyState(VK_LEFT) || GetAsyncKeyState('A')) { player.x_left -= player.vx; } if (GetAsyncKeyState(VK_RIGHT) || GetAsyncKeyState('D')) { player.x_left += player.vx; } if (GetAsyncKeyState(VK_UP) || GetAsyncKeyState('W')) { player.y_bottom -= player.vy; } if (GetAsyncKeyState(VK_DOWN) || GetAsyncKeyState('S')) { player.y_bottom += player.vy; } } }
Enumeration type state switching
Matchmaker can stand to the left, stand to the right, run to the left, run to the right, jump to the left, jump to the right and die:
Add a new member variable in the Player class to store the right standing image, left standing image and role status:
IMAGE im_standright; IMAGE im_standleft; PlayerStatus playerStatus; // current state
Initialize the new member variable in initialize():
loadimage(&im_standright, _T("standright.png")); loadimage(&im_standleft, _T("standleft.png")); playerStatus = standright; im_show = im_standright;
In updateWithInput(), when the keyboard controls the character to move, the character's standing direction should also change:
if (GetAsyncKeyState(VK_LEFT) || GetAsyncKeyState('A')) { player.x_left -= player.vx; player.playerStatus = standleft; } if (GetAsyncKeyState(VK_RIGHT) || GetAsyncKeyState('D')) { player.x_left += player.vx; player.playerStatus = standright; }
Add run animation
Add the member variable IMS in the Player class_ Runright stores 8 pictures of running to the right and initializes:
vector<IMAGE> ims_runright; int animId; void initialize() { loadimage(&im_standright, _T("standright.png")); loadimage(&im_standleft, _T("standleft.png")); playerStatus = standright; im_show = im_standright; width = im_standright.getwidth(); height = im_standright.getheight(); TCHAR filename[80]; for (int i = 0; i <= 7; i++) { swprintf_s(filename, _T("runright%d.png"), i); IMAGE im; loadimage(&im, filename); ims_runright.push_back(im); } animId = 0; updateXY(WIDTH / 2, HEIGHT / 2); vx = 10; vy = 10; }
The new member function runRight() updates the coordinates involved in the character running to the right, and realizes the state switching and animation effect:
void runRight() { x_left += vx; if (playerStatus != runright) { playerStatus = runright; animId = 0; } else { animId++; if (animId >= ims_runright.size()) { animId = 0; } } im_show = ims_runright[animId]; }
Realize jump
Add a member variable in the Player class to store the pictures and gravitational acceleration jumping to the right and left, and initialize:
IMAGE im_jumpright; IMAGE im_jumpleft; float gravity; void initialize() { loadimage(&im_jumpright, _T("jumpright.png")); loadimage(&im_jumpleft, _T("jumpleft.png")); vx = 10; vy = 0; gravity = 3; }
Add beginJump() to control the animation switching and give the character an upward initial speed:
void beginJump() { if (playerStatus != jumpleft && playerStatus != jumpright) // Unable to take off in the air { if (playerStatus == runleft || playerStatus == standleft) { im_show = im_jumpleft; playerStatus = jumpleft; } else if (playerStatus == runright || playerStatus == standright) { im_show = im_jumpright; playerStatus = jumpright; } vy = -30; } }
A new member function updateYcoordinate() is added to automatically complete the free fall movement:
void updateYcoordinate() { if (playerStatus == jumpleft || playerStatus == jumpright) { vy += gravity; y_bottom += vy; if (y_bottom > HEIGHT / 2) { y_bottom = HEIGHT / 2; // Make sure it falls right on the ground if (playerStatus == jumpleft) { playerStatus = standleft; } if (playerStatus == jumpright) { playerStatus = standright; } } } }
Ground class
class Land { public: IMAGE im_land; float left_x, right_x, top_y; float land_width, land_height; void initialize() { loadimage(&im_land, _T("land.png")); land_width = im_land.getwidth(); land_height = im_land.getheight(); left_x = WIDTH / 2; right_x = left_x + land_width; top_y = HEIGHT / 2; } void draw() { putimage(left_x, top_y, &im_land); } };
Scene class
The scene class includes background pictures and multiple ground objects
class Scene { public: IMAGE im_bk; vector<Land> lands; void draw() { putimage(-100, -100, &im_bk); for (int i = 0; i < lands.size(); i++) { lands[i].draw(); } } void initialize() { loadimage(&im_bk, _T("landscape1.png")); for (int i = 1; i <= 6; i++) { Land land; land.initialize(); land.left_x = i * land.land_width; land.top_y = (i - 1) / 6.0 * HEIGHT; lands.push_back(land); } } };
collision detection
Generate some random ground:
void initialize() { loadimage(&im_bk, _T("landscape1.png")); lands.clear(); for (int i = 1; i < 10; i++) { Land land; land.initialize(); land.left_x = i * land.land_width; land.right_x = land.left_x + land.land_width; land.top_y = HEIGHT / 2 + rand() % 2 * HEIGHT / 10; lands.push_back(land); } }
Add the member function isOnLand() for the Player class to judge whether the character is on the ground:
int isOnLand(Land& land, float ySpeed) { float x_right = x_left + width; if (ySpeed <= 0) { ySpeed = 0; } if (land.left_x - x_left <= width * 0.6 && x_right - land.right_x <= width * 0.6 && abs(y_bottom - land.top_y) <= 5 + ySpeed) { return 1; } else { return 0; } }
Add the member function isNotAllLands() to judge whether the character is not on all the ground:
int isNotOnAllLands(vector<Land>& lands, float speed) { for (int i = 0; i < lands.size(); i++) { if (isOnLand(lands[i], speed)) { return 0; } } return 1; }
Modify runRight() and runLeft(). If the character is not on any ground, enter the jumping state:
void runRight(Scene& scene) { x_left += vx; if (isNotOnAllLands(scene.lands, vy)) { im_show = im_jumpright; playerStatus = jumpright; return; } } void runLeft(Scene& scene) { x_left -= vx; if (isNotOnAllLands(scene.lands, vy)) { im_show = im_jumpleft; playerStatus = jumpright; return; } }
Modify updateYcoordinate() to make the character touch the ground and then stop falling:
void updateYcoordinate(Scene& scene) { if (playerStatus == jumpleft || playerStatus == jumpright) { vy += gravity; y_bottom += vy; for (int i = 0; i < scene.lands.size(); i++) { if (isOnLand(scene.lands[i], vy)) { y_bottom = scene.lands[i].top_y; if (playerStatus == jumpleft) { playerStatus = standleft; } if (playerStatus == jumpright) { playerStatus = standright; } break; } } } }
Realize relative motion
Keep the character's position unchanged, while the background and ground move relative:
class Land { void draw(float px, float py) { putimage(left_x - px, top_y - py, &im_land); // Draw the ground with an offset } }; class Scene { void draw(float px, float py) { putimage(-px / 20, -100 - py / 20, &im_bk); // Draw the background with an offset to achieve the foreground and background for (int i = 0; i < lands.size(); i++) { lands[i].draw(px, py); } } } class Player { void draw() { putimagePng(WIDTH / 2, HEIGHT / 2 - height, &im_show); // Character position does not move } }
Endless levels and victory judgment
In updateWithoutInput(), the character runs to the last ground. This level wins the game, and the level increases by 1 to enter the next level; The character falls to the bottom of the screen and the game fails. Restart this level:
void updateWithoutInput() { player.updateYcoordinate(scene); int landSize = scene.lands.size(); if (player.x_left > scene.lands[landSize - 1].left_x && abs(player.y_bottom - scene.lands[landSize - 1].top_y) <= 2) // Game victory { scene.lastlevel = scene.level; scene.level++; scene.initialize(); player.initialize(); } else if (player.y_bottom > 1.5 * HEIGHT) // The character falls to the bottom of the screen and the game fails { scene.lastlevel = scene.level; scene.initialize(); player.initialize(); } }
With the increase of level, the generated map is larger and the ground is more discontinuous:
void initialize() { loadimage(&im_bk, _T("landscape1.png")); if (lands.size() == 0) // By default, it starts from the first level { level = 1; lastlevel = 1; } if (lands.size() == 0 || level > lastlevel) // If you do not upgrade, the scene is not regenerated { lands.clear(); Land land1; // The first level should be under the game character land1.initialize(); lands.push_back(land1); for (int i = 1; i < 10 + level * 2; i++) { Land land2; land2.initialize(); int r1 = randBetweenMinMax(1, 30); if (r1 > level) { land2.left_x = land1.left_x + land2.land_width; } else { land2.left_x = land1.left_x + 2 * land2.land_width; } int r3 = randBetweenMinMax(1, 20); if (r1 > level) { land2.top_y = land1.top_y; } else { int r3 = randBetweenMinMax(-1, 1); land2.top_y = WIDTH / 2 + HEIGHT / 10 * r3; } land2.right_x = land2.left_x + land2.land_width; lands.push_back(land2); land1 = land2; } } }
Enemy human
class Enemy { public: IMAGE im_enemy; float x, y; float enemy_width, enemy_height; float x_min, x_max; // Maximum and minimum x coordinates of enemy movement float vx; // Velocity in x direction void initialize() { loadimage(&im_enemy, _T("bat.png")); enemy_width = im_enemy.getwidth(); enemy_height = im_enemy.getheight(); } void draw(float px, float py) { putimagePng(x - enemy_width / 2 - px, y - enemy_height / 2 - py, &im_enemy); } void update() { x += vx; if (x > x_max || x < x_min) { vx = -vx; } } };
Add Enemy variable in the scene and initialize:
vector<Enemy> enemies; void draw(float px, float py) { putimage(-px / 20, -100 - py / 20, &im_bk); // Draw the background with an offset to achieve the foreground and background for (int i = 0; i < lands.size(); i++) { lands[i].draw(px, py); } for (int i = 0; i < enemies.size(); i++) { enemies[i].draw(px, py); } } void initialize() { if (lands.size() == 0 || level > lastlevel) // If you do not upgrade, the scene is not regenerated { enemies.clear(); int numEnemy = (level + 3) / 5; int idStep = lands.size() / (numEnemy + 1); // Enemies are evenly distributed in the scene for (int j = 1; j <= numEnemy; j++) { Enemy enemy; enemy.initialize(); int landId = j * idStep; enemy.x = lands[landId].left_x + lands[landId].land_width / 2; enemy.y = lands[landId].top_y - enemy.enemy_height; float movingRange = enemy.enemy_width * (3 + level / 15.0); enemy.x_min = enemy.x - movingRange; enemy.x_max = enemy.x + movingRange; enemy.vx = 2 + level / 10.0; enemies.push_back(enemy); } } }
In the Player class, add the member function isCollideEnemy() to judge whether the character collides with the bat:
int isCollideEnemy(Enemy& enemy) { float x_center = x_left + width / 2; float y_center = y_bottom - height / 2; if (abs(enemy.x - x_center) <= enemy.enemy_width * 0.5 && abs(enemy.y - y_center) <= enemy.enemy_height * 0.5) { return 1; } else { return 0; } }
In updateWithoutInput(), make all enemies move horizontally. If they collide with characters, the game fails:
for (int i = 0; i < scene.enemies.size(); i++) { scene.enemies[i].update(); // Enemy automatically updates location if (player.isCollideEnemy(scene.enemies[i])) { scene.lastlevel = scene.level; scene.initialize(); player.initialize(); } }
Add sound effects
Add background music in startup():
mciSendString(_T("open game_music.mp3 alias bkmusic"), NULL, 0, NULL); mciSendString(_T("play bkmusic repeat"), NULL, 0, NULL);
Add victory sound:
PlayMusicOnce(_T("success.mp3"));
Failed to add sound effect:
PlayMusicOnce(_T("warning.mp3"));
Add Pentagram
Place a five pointed star at the end of each level:
IMAGE im_star; void draw(float px, float py) { // Place a five pointed star above the last ground putimagePng(lands[lands.size() - 1].left_x + im_star.getwidth() - px, lands[lands.size() - 1].top_y - im_star.getheight() - py, &im_star); }
Complete code
#include <graphics.h> #include <conio.h> #include <time.h> #include "EasyXPng.h" #include "Timer.h" #include <vector> using namespace std; #pragma comment(lib,"Winmm.lib") #define WIDTH 800 #define HEIGHT 600 enum PlayerStatus { standleft, standright, runleft, runright, jumpleft, jumpright, die }; int randBetweenMinMax(int min, int max) { int r = rand() % (max - min + 1) + min; return r; } void PlayMusicOnce(TCHAR fileName[80]) // Play music function once { TCHAR cmdString1[50]; swprintf_s(cmdString1, _T("open %s alias tmpmusic"), fileName); // Generate command string mciSendString(_T("close tmpmusic"), NULL, 0, NULL); // Turn off the previous music first mciSendString(cmdString1, NULL, 0, NULL); // Turn on the music mciSendString(_T("play tmpmusic"), NULL, 0, NULL); // Play only once } class Land { public: IMAGE im_land; float left_x, right_x, top_y; float land_width, land_height; void initialize() { loadimage(&im_land, _T("land.png")); land_width = im_land.getwidth(); land_height = im_land.getheight(); left_x = WIDTH / 2; right_x = left_x + land_width; top_y = HEIGHT / 2; } void draw(float px, float py) { putimage(left_x - px, top_y - py, &im_land); // Draw the ground with an offset } }; class Enemy { public: IMAGE im_enemy; float x, y; float enemy_width, enemy_height; float x_min, x_max; // Maximum and minimum x coordinates of enemy movement float vx; // Velocity in x direction void initialize() { loadimage(&im_enemy, _T("bat.png")); enemy_width = im_enemy.getwidth(); enemy_height = im_enemy.getheight(); } void draw(float px, float py) { putimagePng(x - enemy_width / 2 - px, y - enemy_height / 2 - py, &im_enemy); } void update() { x += vx; if (x > x_max || x < x_min) { vx = -vx; } } }; class Scene { public: IMAGE im_bk; IMAGE im_star; vector<Land> lands; vector<Enemy> enemies; int level; int lastlevel; void draw(float px, float py) { putimage(-px / 20, -100 - py / 20, &im_bk); // Draw the background with an offset to achieve the foreground and background for (int i = 0; i < lands.size(); i++) { lands[i].draw(px, py); } for (int i = 0; i < enemies.size(); i++) { enemies[i].draw(px, py); } // Place a five pointed star above the last ground putimagePng(lands[lands.size() - 1].left_x + im_star.getwidth() - px, lands[lands.size() - 1].top_y - im_star.getheight() - py, &im_star); // Show which level this is TCHAR s[20]; // character string setbkmode(TRANSPARENT); // Text transparent display swprintf_s(s, _T("The first %d shut"), level); // Generate text string settextcolor(RGB(0, 50, 200)); // Set text color settextstyle(30, 0, _T("Blackbody")); // Set text size and font outtextxy(WIDTH * 0.45, 30, s); // Output text } void initialize() { TCHAR filename[80]; int i = level % 9 + 1; swprintf_s(filename, _T("landscape%d.png"), i); loadimage(&im_bk, filename); loadimage(&im_star, _T("star.png")); if (lands.size() == 0) // By default, it starts from the first level { level = 1; lastlevel = 1; } if (lands.size() == 0 || level > lastlevel) // If you do not upgrade, the scene is not regenerated { lands.clear(); Land land1; // The first level should be under the game character land1.initialize(); lands.push_back(land1); for (int i = 1; i < 10 + level * 2; i++) { Land land2; land2.initialize(); int r1 = randBetweenMinMax(1, 30); if (r1 > level) { land2.left_x = land1.left_x + land2.land_width; } else { land2.left_x = land1.left_x + 2 * land2.land_width; } int r3 = randBetweenMinMax(1, 20); if (r1 > level) { land2.top_y = land1.top_y; } else { int r3 = randBetweenMinMax(-1, 1); land2.top_y = WIDTH / 2 + HEIGHT / 10 * r3; } land2.right_x = land2.left_x + land2.land_width; lands.push_back(land2); land1 = land2; } enemies.clear(); int numEnemy = (level + 3) / 5; int idStep = lands.size() / (numEnemy + 1); // Enemies are evenly distributed in the scene for (int j = 1; j <= numEnemy; j++) { Enemy enemy; enemy.initialize(); int landId = j * idStep; enemy.x = lands[landId].left_x + lands[landId].land_width / 2; enemy.y = lands[landId].top_y - enemy.enemy_height; float movingRange = enemy.enemy_width * (3 + level / 15.0); enemy.x_min = enemy.x - movingRange; enemy.x_max = enemy.x + movingRange; enemy.vx = 2 + level / 10.0; enemies.push_back(enemy); } } } }; class Player { public: IMAGE im_show; // Image to be displayed at the current time IMAGE im_standright; IMAGE im_standleft; IMAGE im_jumpright; IMAGE im_jumpleft; vector<IMAGE> ims_runright; vector<IMAGE> ims_runleft; int animId; PlayerStatus playerStatus; // current state float x_left, y_bottom; // Lower left corner position float vx, vy; // speed float gravity; float width, height; // Width and height of the picture void draw() { putimagePng(WIDTH / 2, HEIGHT / 2 - height, &im_show); // Character position does not move } void initialize() { ims_runleft.clear(); ims_runright.clear(); loadimage(&im_standright, _T("standright.png")); loadimage(&im_standleft, _T("standleft.png")); loadimage(&im_jumpright, _T("jumpright.png")); loadimage(&im_jumpleft, _T("jumpleft.png")); playerStatus = standright; im_show = im_standright; width = im_standright.getwidth(); height = im_standright.getheight(); TCHAR filename[80]; for (int i = 0; i <= 7; i++) { swprintf_s(filename, _T("runright%d.png"), i); IMAGE im; loadimage(&im, filename); ims_runright.push_back(im); } for (int i = 0; i <= 7; i++) { swprintf_s(filename, _T("runleft%d.png"), i); IMAGE im; loadimage(&im, filename); ims_runleft.push_back(im); } animId = 0; updateXY(WIDTH / 2, HEIGHT / 2); vx = 10; vy = 0; gravity = 3; } void updateXY(float mx, float my) { x_left = mx; y_bottom = my; } void runRight(Scene& scene) { x_left += vx; if (isNotOnAllLands(scene.lands, vy)) { im_show = im_jumpright; playerStatus = jumpright; return; } if (playerStatus == jumpleft || playerStatus == jumpright) // If it is take-off state { im_show = im_jumpright; } else { if (playerStatus != runright) { playerStatus = runright; animId = 0; } else { animId++; if (animId >= ims_runright.size()) { animId = 0; } } im_show = ims_runright[animId]; } } void runLeft(Scene& scene) { x_left -= vx; if (isNotOnAllLands(scene.lands, vy)) { im_show = im_jumpleft; playerStatus = jumpright; return; } if (playerStatus == jumpleft || playerStatus == jumpright) // If it is take-off state { im_show = im_jumpleft; } else { if (playerStatus != runleft) { playerStatus = runleft; animId = 0; } else { animId++; if (animId >= ims_runleft.size()) { animId = 0; } } im_show = ims_runleft[animId]; } } void standStill() { if (playerStatus == runleft || playerStatus == standleft) { im_show = im_standleft; } else if (playerStatus == runright || playerStatus == standright) { im_show = im_standright; } } void beginJump() { if (playerStatus != jumpleft && playerStatus != jumpright) // Unable to take off in the air { if (playerStatus == runleft || playerStatus == standleft) { im_show = im_jumpleft; playerStatus = jumpleft; } else if (playerStatus == runright || playerStatus == standright) { im_show = im_jumpright; playerStatus = jumpright; } vy = -30; } } int isCollideEnemy(Enemy& enemy) { float x_center = x_left + width / 2; float y_center = y_bottom - height / 2; if (abs(enemy.x - x_center) <= enemy.enemy_width * 0.5 && abs(enemy.y - y_center) <= enemy.enemy_height * 0.5) { return 1; } else { return 0; } } int isOnLand(Land& land, float ySpeed) { float x_right = x_left + width; if (ySpeed <= 0) { ySpeed = 0; } if (land.left_x - x_left <= width * 0.6 && x_right - land.right_x <= width * 0.6 && abs(y_bottom - land.top_y) <= 5 + ySpeed) { return 1; } else { return 0; } } int isNotOnAllLands(vector<Land>& lands, float speed) { for (int i = 0; i < lands.size(); i++) { if (isOnLand(lands[i], speed)) { return 0; } } return 1; } void updateYcoordinate(Scene& scene) { if (playerStatus == jumpleft || playerStatus == jumpright) { vy += gravity; y_bottom += vy; for (int i = 0; i < scene.lands.size(); i++) { if (isOnLand(scene.lands[i], vy)) { y_bottom = scene.lands[i].top_y; if (playerStatus == jumpleft) { playerStatus = standleft; } if (playerStatus == jumpright) { playerStatus = standright; } break; } } } } }; Player player; Scene scene; Timer timer; void startup() { mciSendString(_T("open game_music.mp3 alias bkmusic"), NULL, 0, NULL); mciSendString(_T("play bkmusic repeat"), NULL, 0, NULL); srand(time(0)); scene.initialize(); player.initialize(); initgraph(WIDTH, HEIGHT); BeginBatchDraw(); } void show() { scene.draw(player.x_left - WIDTH / 2, player.y_bottom - HEIGHT / 2); player.draw(); FlushBatchDraw(); timer.Sleep(50); } void updateWithoutInput() { player.updateYcoordinate(scene); int landSize = scene.lands.size(); if (player.x_left > scene.lands[landSize - 1].left_x && abs(player.y_bottom - scene.lands[landSize - 1].top_y) <= 2) // Game victory { PlayMusicOnce((TCHAR*)_T("success.mp3")); scene.lastlevel = scene.level; scene.level++; scene.initialize(); player.initialize(); } else if (player.y_bottom > 1.5 * HEIGHT) // The character falls to the bottom of the screen and the game fails { PlayMusicOnce((TCHAR*)_T("warning.mp3")); scene.lastlevel = scene.level; scene.initialize(); player.initialize(); } for (int i = 0; i < scene.enemies.size(); i++) { scene.enemies[i].update(); // Enemy automatically updates location if (player.isCollideEnemy(scene.enemies[i])) { PlayMusicOnce((TCHAR*)_T("warning.mp3")); scene.lastlevel = scene.level; scene.initialize(); player.initialize(); } } } void updateWithInput() { player.standStill(); // By default, the game character stands still to the left or right if (_kbhit()) { if (GetAsyncKeyState(VK_RIGHT) || GetAsyncKeyState('D')) { player.runRight(scene); } else if (GetAsyncKeyState(VK_LEFT) || GetAsyncKeyState('A')) { player.runLeft(scene); } if (GetAsyncKeyState(VK_UP) || GetAsyncKeyState('W')) { player.beginJump(); } } } int main() { startup(); while (1) { show(); updateWithoutInput(); updateWithInput(); } return 0; }