Touhou 开发手记


Github传送门

效果图

// To-Do List 自带传送门

To-Do List

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

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

1.配置

SFML 2.4.2

2.显示静止飞机

1
2
3
4
5
6
7
8
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

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

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

4.左右键控制

1
2
3
4
5
6
7
8
9
10
11
12
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.限制区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.发射子弹&越界回收

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
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.时间轴管理

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
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.时间轴事件

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
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.碰撞检测

1
2
3
4
5
6
7
8
9
10
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.碰撞处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.敌机爆炸过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.计分和显示

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
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.敌机炮弹处理

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.自机判定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}