iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
Software Development

三十天內用C++寫出一個小遊戲系列 第 29

Day 29 - 這個遊戲製作人瘋了吧

Intro

這次寫了一個小遊戲。裡面包含了前幾次練習的功能,但是有些東西是用 class 來寫出來,感覺很新奇!

這次的遊戲大概就是像宇宙飛船一樣,右邊會跑出移動中的敵人,我們在左邊要射擊他們。

解惑中

因為我之前對 sprite 還有 texture 的概念不太了解,所以查了一下這一篇

所以可以簡單的分類說

Sprite : 是一個物件,可以透過 sprite 來做各種操作!

Texture: 就是外面的包裝紙,你想要怎樣的圖案,就貼在 sprite 上面!

下面是我試做了一個小小的實驗,看看她到底是怎麼運作的。

#include<iostream>
#include<SFML\Graphics.hpp>
#include<SFML\Window.hpp>
#include<SFML\System.hpp>
#include<sstream>
#include<cstdlib>
#include<math.h>
#include<vector>
using namespace sf;
using namespace std;

int main()
{

	RenderWindow window(VideoMode(800, 600), "Space war");
	window.setFramerateLimit(200);

	Texture doggeTex;
	Sprite dogge;
	if (!doggeTex.loadFromFile("textures/dogge.png"))
		throw "Could not find file: dogge.png!";

	dogge.setTexture(doggeTex);
	dogge.setScale(Vector2f(0.15f, 0.15f));

	// 【game loop】

	while (window.isOpen()) // hp  < 0 的時候就 shut down
	{
		Event event;
		while (window.pollEvent(event))
		{
			if (event.type == Event::Closed)
				window.close();

			if (event.type == Event::KeyPressed && event.key.code == Keyboard::Escape)
				window.close();
		}

		/*===========================================================//
		   【Update】
		//===========================================================*/

			if (Keyboard::isKeyPressed(Keyboard::W))
				dogge.move(0.f, -5.f);
			if (Keyboard::isKeyPressed(Keyboard::S))
				dogge.move(0.f, 5.f);
			if (Keyboard::isKeyPressed(Keyboard::D))
				dogge.move(5.f, 0.f);
			if (Keyboard::isKeyPressed(Keyboard::A))
				dogge.move(-5.f, 0.f);

		/*===========================================================//
		   【Draw】
		//===========================================================*/
		window.clear();
		window.draw(dogge);
		window.display();

	}

}

了解了 Sprite 還有 texture 的概念我們就可以開始了!

一個頗大的遊戲

首先我們要宣告三個 class ,包含 player、enemy、還有 bullets

class Bullet
{
public:
	Sprite shape;

	Bullet(Texture* texture, Vector2f pos)
	{
		this->shape.setTexture(*texture);

		this->shape.setScale(Vector2f(0.1f, 0.1f));
	
		this->shape.setPosition(pos);
	}

	~Bullet() {}
};

class Player
{
public:
	Sprite shape;
	Texture* texture;
	
	int HP;
	int HPMax;

	vector<Bullet> bullets;

	Player(Texture* texture)
	{
		this->HPMax = 10;
		this->HP = this->HPMax;

		this->texture = texture;
		this->shape.setTexture(*texture);

		this->shape.setScale(Vector2f(0.15f, 0.15f));
	}
	~Player() {}

};

class Enemy
{
public:
	Sprite shape;

	int HP;
	int HPMax;

	Enemy(Texture* texture, Vector2u windowSize)
	{
		this->HPMax = rand() % 3 + 1; // 隨機血量 -> 1~4
		this->HP = this->HPMax;

		this->shape.setTexture(*texture);

		this->shape.setScale(0.3f, 0.3f);

		this->shape.setPosition(windowSize.x - this->shape.getGlobalBounds().width, rand() % (int)(windowSize.y - this->shape.getGlobalBounds().height));
	}

	~ Enemy() {}
};

這三個 class 中可以看到有他們的血量,還有關於 texture size 方面的設定,這些都可以在設定 class 的時候設定好。比較不好的地方是,現在他全部都是用 public (雖然只有我一個人再寫),但有些他們獨有的特性還是盡量要用 private 的方式寫會比較好。

srand(time(NULL)); // 這一部分是為了 random 函數的,但詳細不太清楚

	RenderWindow window(VideoMode(800, 600), "Space war");
	window.setFramerateLimit(200);
	
	/*===========================================================//
		Init Text
	//===========================================================*/
	Font font;
	font.loadFromFile("font/Arial.ttf");

	/*===========================================================//
		Init Textures
	//===========================================================*/
	Texture playerTex;
	playerTex.loadFromFile("textures/spaceship.png");

	Texture enemyTex;
	enemyTex.loadFromFile("textures/enemies.png");

	Texture bulletTex;
	bulletTex.loadFromFile("textures/MissileRight.png");

    /*===========================================================//
	   UI init
    //===========================================================*/ 
	Text scoreText;
	scoreText.setFont(font);
	scoreText.setCharacterSize(20);
	scoreText.setFillColor(Color::White);
	scoreText.setPosition(10.f, 10.f);

	Text gameOverText;
	gameOverText.setFont(font);
	gameOverText.setCharacterSize(30);
	gameOverText.setFillColor(Color::Red);
	gameOverText.setPosition(100.f, window.getSize().y / 2);
	gameOverText.setString("Game Over!!!!");

    /*===========================================================//
	   Player init
    //===========================================================*/ 
	int score = 0;

	Player player(&playerTex);
	int shootTimer = 20;
	Text HpText;
	HpText.setFont(font);
	HpText.setCharacterSize(12);
	HpText.setFillColor(Color::White);

	/*===========================================================//
	    Enemy init
	//===========================================================*/
	int enemySpawnTimer = 0;
	vector<Enemy> enemies;
	enemies.push_back(Enemy(&enemyTex, window.getSize()));
	Text eHpText;
	eHpText.setFont(font);
	eHpText.setCharacterSize(12);
	eHpText.setFillColor(Color::White);

接下來這一段是在初始化,也就是設定文字,設定 player, enemy, 還有 bullet 的一些設定。

	/*===========================================================//
		【game loop】
	//===========================================================*/ 

	while (window.isOpen()) // hp  < 0 的時候就 shut down
	{
		Event event;
		while (window.pollEvent(event))
		{
			if (event.type == Event::Closed)
				window.close();

			if (event.type == Event::KeyPressed && event.key.code == Keyboard::Escape)
				window.close();
		}

		if (player.HP > 0)
		{

		
		
		/*===========================================================//
		   【Update】 player 
		//===========================================================*/
			// movement

			int speedInc = 0;

			if (Keyboard::isKeyPressed(Keyboard::W))
				player.shape.move(0.f, -5.f - speedInc);
			if (Keyboard::isKeyPressed(Keyboard::S))
				player.shape.move(0.f, 5.f + speedInc);
			if (Keyboard::isKeyPressed(Keyboard::D))
				player.shape.move(5.f + speedInc, 0.f);
			if (Keyboard::isKeyPressed(Keyboard::A))
				player.shape.move(-5.f - speedInc, 0.f);

			// collision with window
			if (player.shape.getPosition().x <= 0) // left
				player.shape.setPosition(0.f, player.shape.getPosition().y);
			if (player.shape.getPosition().x >= window.getSize().x - player.shape.getGlobalBounds().width) // right
				player.shape.setPosition(window.getSize().x - player.shape.getGlobalBounds().width, player.shape.getPosition().y);
			if (player.shape.getPosition().y <= 0) // Top
				player.shape.setPosition(player.shape.getPosition().x, 0.f);
			if (player.shape.getPosition().y >= window.getSize().y - player.shape.getGlobalBounds().height) // bottom
				player.shape.setPosition(player.shape.getPosition().x, window.getSize().y - player.shape.getGlobalBounds().height);

			// Text set
			HpText.setPosition(player.shape.getPosition().x, player.shape.getPosition().y - HpText.getGlobalBounds().height);
			HpText.setString(to_string(player.HP) + "/" + to_string(player.HPMax));
		

		/*===========================================================//
		   【Update】 controls
		//===========================================================*/
	
			if (shootTimer < 20)
				shootTimer++;
		
			if (Mouse::isButtonPressed(Mouse::Left) && shootTimer >= 20)
			{
				player.bullets.push_back(Bullet(&bulletTex, player.shape.getPosition()));
				shootTimer = 0; // reset timer
			}

		/*===========================================================//
		   【Update】 Bullet 
		//===========================================================*/

			for (size_t i = 0; i < player.bullets.size(); i++)
			{
				// Move
				player.bullets[i].shape.move(20.f, 0.f);

				// Out of window bounds
				if (player.bullets[i].shape.getPosition().x > window.getSize().x)
				{
					player.bullets.erase(player.bullets.begin() + i);
					break;
				}
				
				// enemy collision
				for (size_t k = 0; k < enemies.size(); k++)
				{
					if (player.bullets[i].shape.getGlobalBounds().intersects(enemies[k].shape.getGlobalBounds()))
					{	
						if (enemies[k].HP <= 1)
						{
							score += enemies[k].HPMax;
							enemies.erase(enemies.begin() + k);
						}
						
						else
							enemies[k].HP--;

						player.bullets.erase(player.bullets.begin() + i);
						break;
					}
				}
			}	
		/*===========================================================//
		   【Update】 Enemy
		//===========================================================*/

			if (enemySpawnTimer < 30)
				enemySpawnTimer++;
			if (enemySpawnTimer >= 30)
			{
				enemies.push_back(Enemy(&enemyTex, window.getSize()));
				enemySpawnTimer = 0;
			}

			for (size_t i = 0; i < enemies.size(); i++)
			{
				enemies[i].shape.move(-2.3f, 0.f);

				if (enemies[i].shape.getPosition().x <= 0 - enemies[i].shape.getGlobalBounds().width)
				{ 
					enemies.erase(enemies.begin() + i);
					break;
				}
				if (enemies[i].shape.getGlobalBounds().intersects(player.shape.getGlobalBounds()))
				{
					enemies.erase(enemies.begin() + i);
				
					player.HP--;
					break;
				}
			}

			/*===========================================================//
			   【Update】 UI
			//===========================================================*/

			scoreText.setString("Score: " + to_string(score));

		}

	/*===========================================================//
	   【Draw】
	//===========================================================*/
		window.clear();

    /*===========================================================//
	   【Draw】  player
    //===========================================================*/
		window.draw(player.shape);
		
    /*===========================================================//
	   【Draw】 bullets
    //===========================================================*/		
		for (size_t i = 0; i < player.bullets.size(); i++)
		{
			window.draw(player.bullets[i].shape);
		}
		
    /*===========================================================//
	   【Draw】 Enemies
    //===========================================================*/
		for (size_t i = 0; i < enemies.size(); i++)
		{
			eHpText.setString(to_string(enemies[i].HP) + "/" + to_string(enemies[i].HPMax));
			eHpText.setPosition(enemies[i].shape.getPosition().x, enemies[i].shape.getPosition().y - eHpText.getGlobalBounds().height);
			window.draw(eHpText);
			window.draw(enemies[i].shape);
		}
    /*===========================================================//
	    【Draw】 UI
    //===========================================================*/  
		window.draw(scoreText);
		window.draw(HpText);

		if (player.HP <= 0)
			window.draw(gameOverText);
			
		window.display();

	}

再來這一段,就是遊戲的 loop,

首先可以看到 player 的部分

/*===========================================================//
		   【Update】 player 
		//===========================================================*/
			// movement

			int speedInc = 0;

			if (Keyboard::isKeyPressed(Keyboard::W))
				player.shape.move(0.f, -5.f - speedInc);
			if (Keyboard::isKeyPressed(Keyboard::S))
				player.shape.move(0.f, 5.f + speedInc);
			if (Keyboard::isKeyPressed(Keyboard::D))
				player.shape.move(5.f + speedInc, 0.f);
			if (Keyboard::isKeyPressed(Keyboard::A))
				player.shape.move(-5.f - speedInc, 0.f);

			// collision with window
			if (player.shape.getPosition().x <= 0) // left
				player.shape.setPosition(0.f, player.shape.getPosition().y);
			if (player.shape.getPosition().x >= window.getSize().x - player.shape.getGlobalBounds().width) // right
				player.shape.setPosition(window.getSize().x - player.shape.getGlobalBounds().width, player.shape.getPosition().y);
			if (player.shape.getPosition().y <= 0) // Top
				player.shape.setPosition(player.shape.getPosition().x, 0.f);
			if (player.shape.getPosition().y >= window.getSize().y - player.shape.getGlobalBounds().height) // bottom
				player.shape.setPosition(player.shape.getPosition().x, window.getSize().y - player.shape.getGlobalBounds().height);

			// Text set
			HpText.setPosition(player.shape.getPosition().x, player.shape.getPosition().y - HpText.getGlobalBounds().height);
			HpText.setString(to_string(player.HP) + "/" + to_string(player.HPMax));

一樣是包含了上下左右的移動,還有與 window 撞到的反應,最後是會顯示再 player 頭上的 血量。

下面這段則是 bullet 與敵人的碰撞還有移動。

	/*===========================================================//
		   【Update】 Bullet 
		//===========================================================*/

			for (size_t i = 0; i < player.bullets.size(); i++)
			{
				// Move
				player.bullets[i].shape.move(20.f, 0.f);

				// Out of window bounds
				if (player.bullets[i].shape.getPosition().x > window.getSize().x)
				{
					player.bullets.erase(player.bullets.begin() + i);
					break;
				}
				
				// enemy collision
				for (size_t k = 0; k < enemies.size(); k++)
				{
					if (player.bullets[i].shape.getGlobalBounds().intersects(enemies[k].shape.getGlobalBounds()))
					{	
						if (enemies[k].HP <= 1)
						{
							score += enemies[k].HPMax;
							enemies.erase(enemies.begin() + k);
						}
						
						else
							enemies[k].HP--;

						player.bullets.erase(player.bullets.begin() + i);
						break;
					}
				}
			}	
		/*===========================================================//
		   【Update】 Enemy
		//===========================================================*/

			if (enemySpawnTimer < 30)
				enemySpawnTimer++;
			if (enemySpawnTimer >= 30)
			{
				enemies.push_back(Enemy(&enemyTex, window.getSize()));
				enemySpawnTimer = 0;
			}

			for (size_t i = 0; i < enemies.size(); i++)
			{
				enemies[i].shape.move(-2.3f, 0.f);

				if (enemies[i].shape.getPosition().x <= 0 - enemies[i].shape.getGlobalBounds().width)
				{ 
					enemies.erase(enemies.begin() + i);
					break;
				}
				if (enemies[i].shape.getGlobalBounds().intersects(player.shape.getGlobalBounds()))
				{
					enemies.erase(enemies.begin() + i);
				
					player.HP--;
					break;
				}
			}

值得注意的是,我們在碰撞到的時候都記得要 break loop,否則會當掉。

剩下的就是 一些 draw 的功能了。

小結

簡單的來說,這次的小遊戲包含了前幾次的練習,再加上一些使用 class 的方式。

接下來就是我自己的小遊戲了!!!!!

這個是這次的示範影片

Ref

  • 一樣也是 這個 YouTuber 的影片!

SFML Tutorials


上一篇
Day 28 - 設籍有關涉及射擊的射擊遊戲
下一篇
Day 30 - 故事的最後不是句點,是開始
系列文
三十天內用C++寫出一個小遊戲30

尚未有邦友留言

立即登入留言