All posts
Dev中文

Touhou 开发手记

Kyr1os7 min readDevC++
Sections
16
Images
6
Code
13
Read time
7 min

[Github传送门](https://github.com/Kyrios0/TouHou-Project-Fighters- "github传送门")

效果图

// To-Do List 自带传送门

To-Do List

编号任务(功能)ValueEffort是否已完成
1完成SFML配置,显示“SFML works”01Done
2显示一架静止的飞机于屏幕底部51Done
2+显示僚机5
2+设置背景边框44Done
2+设置背景动画44Done
3背景音乐11Done
4左右键,控制移动飞机103Done
4+移动的动画展示52Done
4+判定点·低速移动22Done
4+判定点动画特效21Done
5限制左右边界11Done
6开炮,显示运动的炮弹57Done
6+子弹·击中音效22Done
6+僚机发射子弹,并附带跟踪效果10
7炮弹飞出边界处理21Done
8随机产生敌机,并向下运动10
8根据时间轴产生敌机,并遵照轨迹方程移动2020Done
8+敌机的移动动画53Done
9敌机飞出边界处理21Done
10碰撞处理(敌机与炮弹碰撞)1015Done
11显示敌机爆炸过程102Done
12爆炸声音21Done
13计分及显示51Done
14敌机炮弹处理101Done
14+首领符卡设计·实现100100Done
14+敌机炮弹音效31Done
15被敌机击中处理(炸毁、3条命)102Done
15+死亡音效和动画101Done
16过关控制(过关需要计分、游戏速度控制)201Done
Total

//以下所有代码均未封装

1.配置

SFML 2.4.2

2.显示静止飞机

sf::RenderWindow mWindow(sf::VideoMode(1280, 960), "TouHou20.0-chs");
sf::Texture Reimu;
if (!Reimu.loadFromFile("E:\\Media\\Sources\\Jpg&Png\\TH\\Ex\\player\\pl00\\pl00.png", sf::IntRect(0, 0, 30, 45)))
{
	puts("Error: Load Reimu failed!");
}
sf::Sprite pl01(Reimu);
mWindow.draw(pl01);

Tips:实际代码中采用了动画效果,会根据当前帧数来选定 Sprite。

3.BGM

if (!music.openFromFile("上海アリス幻樂団 - 不思議なお祓い棒.wav"))
{
	puts("Error: Open 上海アリス幻樂団 - 不思議なお祓い棒.wav failed!");
}
// Play the music
music.play();

Tips:music 不会预先把文件读进缓冲区,所以适合大文件(1分钟以上?)的播放,在小音效的处理上应该使用预先把文件读进缓冲区的 sound,可以极大的节省CPU的开销(实测大概是20倍的差距)。

4.左右键控制

if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
{
    // move left...
}
else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
{
    // move right...
}
else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Escape))
{
    // quit...
}

Tips:这里在实际的代码中其实采取了一个监听和处理分开的策略,感觉这样结构更清晰一下,效率也更高(大概)。

5.限制区域

if (mIsMovingUp == true && player.hero.getPosition().y > 40)
{
	player.hero.move(0.0, -player.speed);
}
if (mIsMovingDown == true && player.hero.getPosition().y < 850)
{
	player.hero.move(0.0, player.speed);
}
if (mIsMovingLeft == true && player.hero.getPosition().x > 69)
{
	player.hero.move(-player.speed, 0.0);
}
if (mIsMovingRight == true && player.hero.getPosition().x < 751)
{
	player.hero.move(player.speed, 0.0);
}

Tips:STL 里的 list 下的 remove_if 因为其内部实现的原因,是不支持函数重载的,所以后来写了一个针对 FO 类的越界判定函数。之后最好能把两个函数整合一下,都针对 FO 对象来判定会更灵活一些。

6&7&9.发射子弹&越界回收

if (mIsFire)
{
	//playerAmmo = (mIsGrazing) ? player.LSAmmo : player.HSAmmo;
	if (i % 2 == 1)
	{
		player.LSAmmo.setPosition(sf::Vector2f(player.hero.getPosition().x + 4, player.hero.getPosition().y + 100));
		playerBullets.push_back(player.LSAmmo);
		layer.LSAmmo.setPosition(sf::Vector2f(player.hero.getPosition().x + 20, player.hero.getPosition().y + 100));
		playerBullets.push_back(player.LSAmmo);
		player.HSAmmo.setPosition(sf::Vector2f(player.hero.getPosition().x + 4, player.hero.getPosition().y + 130));
		playerBullets.push_back(player.HSAmmo);
		player.HSAmmo.setPosition(sf::Vector2f(player.hero.getPosition().x + 20, player.hero.getPosition().y + 130));
		playerBullets.push_back(player.HSAmmo);
	}
}
playerBullets.remove_if(isOutOfBoard);
for (list<sf::Sprite>::iterator it = playerBullets.begin(); it != playerBullets.end(); it++)
{
	it->setPosition(it->getPosition().x, it->getPosition().y - 60);

	mWindow.draw(*it);
}

bool isOutOfBoard(sf::Sprite value)
{
	if (value.getPosition().y <= 136)
	{
		return true;
	}
	return false;
}

Tips:本来是给自机配备了两种火力模式以供选择的,后来觉得火力太薄了所以干脆一起搭载上了,大概对于敌机来说是很不友好的设定吧(笑)。

8.1&16.时间轴管理

void Game::Stage1()
{
	static sf::Time elapsed1 = clock.restart();
	elapsed1 = clock.getElapsedTime();

	static int evts[20] = { 0 };

	static int curTime = 1;
	if (curTime < elapsed1.asSeconds())
	{
		printf("%.0f\n", elapsed1.asSeconds());
		curTime++;
	}

	switch ((int)elapsed1.asSeconds())
	{
	case 1:
		//pre
		evts[1] = 1;
		break;
	case 12:
		//title
		evts[2] = 1;
		break;
	case 37:
		//wave
		evts[3] = 1;
		break;
	case 50:
		//middle
		evts[4] = 1;
		break;
	case 63:
		//spellCard1
		evts[5] = 1;
		break;
	case 100:
		//boss
		evts[6] = 1;
		break;
}

Tips:如果 Clock 达到了触发条件,就把某个事件开关置 1,新世界的大门就打开了(明明只是奇奇怪怪的弹幕游戏来着)。

8.2.时间轴事件

int Stage01Event01()
{
	static int curFrame = 0;
	curFrame++;
	static list<FO> wave1, wave2;
	double gapTime = 0.4;
	int gapFrame = gapTime * 60;
	static int gap = 0, temp = 0;
	if (curFrame % gapFrame == 1 && curFrame < 17 * gapFrame)
	{
		//wave1
	}
	if (curFrame == 270)
	{
		//wave2
	}
	wave1.remove_if(isFOOutOfBoard);
	for (list<FO>::iterator it = wave1.begin(); it != wave1.end(); it++)
	{
		//Trajectory equation 01
	}
	wave2.remove_if(isFOOutOfBoard);
	for (list<FO>::iterator it = wave2.begin(); it != wave2.end(); it++)
	{
		//Trajectory equation 02
	}
	if (i1 > 15 * 60)
	{
		wave1.clear();//Final clear for accident
		wave2.clear();
		return 1;
	}
	return 0;
}

Tips:记得 return 1; 的时候顺便把门关上,不然可就糟了。

10.1.碰撞检测

bool Game::checkCollision(sf::Sprite obj1, sf::Sprite obj2)
{
	sf::FloatRect f1 = obj1.getGlobalBounds();
	sf::FloatRect f2 = obj2.getGlobalBounds();
	if (f1.intersects(f2))
	{
		return true;
	}
	return false;
}

10.2.碰撞处理

void Game::enemyCollisionProcessing(list<FO>::iterator it)
{
	for (list<sf::Sprite>::iterator itAmmo = playerBullets.begin(); itAmmo != playerBullets.end(); itAmmo++)
	{
		if (checkCollision(it->hero, *itAmmo))
		{
			enemyUnderAttack(it, itAmmo);

			if (it->HealthPoint <= 0)
			{
				enemyCrash(it);
			}
		}
	}
}

Tips:自机用的碰撞处理也是差不多的,稍微改改就能用。

11&12.敌机爆炸过程

void Game::enemyCrash(list<FO>::iterator it)
{
	breakSound.play();
	score += it->score;
	deathEff.setTexture(deathCircle);
	deathEff.setTextureRect(sf::IntRect(64, 0, 64, 64));
	deathEff.setOrigin(32, 32);
	deathEff.setPosition(it->hero.getPosition().x + it->width * 0.25, it->hero.getPosition().y + it->height * 0.25);
	deathEff.setScale(0.1, 0.1);
	deathEffs.push_back(deathEff);
	deathEff.setScale(0.3, 0.06);
	deathEff.setRotation(rand() % 360);
	deathEffs.push_back(deathEff);
	it->hero.setPosition(-100, -100);
}

13.计分和显示

void Game::boardDisplay()
{
	mWindow.draw(front01);//Display main background
	mWindow.draw(front02);
	mWindow.draw(front03);
	mWindow.draw(front04);

	switch (remnant)
	{
	case 3:
		lifeBoard.setTextureRect(sf::IntRect(0, 0, 272, 36));
		break;
	case 2:
		lifeBoard.setTextureRect(sf::IntRect(0, 44, 272, 36));
		break;
	case 1:
		lifeBoard.setTextureRect(sf::IntRect(0, 90, 272, 36));
		break;
	default:
		;
	}

	lifeBoard.setScale(1.5, 1.5);
	lifeBoard.setPosition(830, 300);
	mWindow.draw(lifeBoard);

	static string scoreStr;
	scoreStr = "Score:             ";
	scoreStr += to_string(score);
	tempScore.setString(scoreStr);
	tempScore.setStyle(sf::Text::Italic);
	tempScore.setFont(font);
	tempScore.setCharacterSize(50);
	tempScore.setPosition(840, 50);
	mWindow.draw(tempScore);
}

14.敌机炮弹处理

举了一个随机弹(尖弹)作为例子

void Game::setSharpRandom(list<FO>::iterator it, double speed)
{
	enemyBulletSound.play();
	FO SharpRandom;
	SharpRandom.speed = speed;
	SharpRandom.theta = rand()%360;
	SharpRandom.width = 16;
	SharpRandom.height = 16;
	SharpRandom.hero.setTexture(allBullets1);
	SharpRandom.hero.setTextureRect(sf::IntRect(64, 64, 16, 16));
	SharpRandom.hero.setOrigin(8, 8);
	SharpRandom.hero.setScale(1.5, 1.5);
	SharpRandom.hero.setPosition(it->hero.getPosition().x, it->hero.getPosition().y + it->height);
	SharpRandom.hero.setRotation(SharpRandom.theta  / PI * 180.0 + 90);
	enemyBullets.push_back(SharpRandom);
}

15.自机判定

bool Game::checkPlayerCollision()
{
	sf::Vector2f JP = julgePoint.getPosition();
	JP.x -= 8;
	JP.y -= 8;

	for (list<FO>::iterator it = enemyBullets.begin(); it != enemyBullets.end(); it++)
	{
		sf::FloatRect f = it->hero.getGlobalBounds();

		f.width /= 2.0;
		f.height /= 2.0;
		if (f.contains(JP))
		{
			return true;
		}
	}
	return false;
}

Comments