Created
February 14, 2016 14:50
-
-
Save chobby/1d6994a993f19956f888 to your computer and use it in GitHub Desktop.
テトリス風ゲーム
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# include <Siv3D.hpp> | |
# include <HamFramework.hpp> | |
enum GridState | |
{ | |
I, O, S, Z, J, L, T, Null, Wall | |
}; | |
struct BlockDefinition | |
{ | |
std::array<Point, 4> figure; | |
GridState gridState; | |
int angleLimit; | |
}; | |
const Size worldSize(10 + 2, 20 + 2 + 2); | |
const Size cellSize(20, 20); | |
const std::array<int, 5> scoringFormula = { 0, 10, 50, 200, 500 }; | |
const std::array<BlockDefinition, 7> blockDefinitions = | |
{ { | |
{ { { { -1, 0 },{ 0, 0 },{ 1, 0 },{ 2, 0 } } }, GridState::I, 2 }, | |
{ { { { 0, 0 },{ 1, 0 },{ 0, 1 },{ 1, 1 } } }, GridState::O, 1 }, | |
{ { { { 0, 0 },{ -1, 0 },{ 0, -1 },{ 1, -1 } } }, GridState::S, 2 }, | |
{ { { { 0, 0 },{ 1, 0 },{ 0, -1 },{ -1, -1 } } }, GridState::Z, 2 }, | |
{ { { { 0, 0 },{ 1, 0 },{ -1, 0 },{ -1, -1 } } }, GridState::J, 4 }, | |
{ { { { 0, 0 },{ -1, 0 },{ 1, 0 },{ 1, -1 } } }, GridState::L, 4 }, | |
{ { { { 0, 0 },{ -1, 0 },{ 1, 0 },{ 0, -1 } } }, GridState::T, 4 } | |
} }; | |
const std::array<Color, 9> gridStateColors = { Palette::Lightblue, Palette::Yellow, Palette::Greenyellow, Palette::Red, Palette::Blue, Palette::Orange, Palette::Purple, Color(0, 0, 0), Color(50, 50, 50) }; | |
struct CommonData | |
{ | |
CommonData() | |
{ | |
const auto holdFunction = [](double t) {return Atan(Tan(t * 440 * TwoPi * 2.0)) * 0.05 * Exp(-10.0 * t) * ((t > 0) ? 1.0 : 0.0); }; | |
sounds[0] = Sound(Wave(1.0s, [](double t) {return Sin(t * 440 * TwoPi * 3.0) * Exp(-10.0 * t); })); | |
sounds[1] = Sound(Wave(0.2s, [](double t) {return (Sin(t * 440 * TwoPi) * 0.95 + Sin(t * 440 * TwoPi * 0.25) * 0.05) * Exp(-10.0 * t); })); | |
sounds[2] = Sound(Wave(0.1s, [](double t) {return Sin(t * 440 * TwoPi * 2.0) * Exp(-100.0 * t); })); | |
sounds[3] = Sound(Wave(1.0s, [&](double t) {return holdFunction(t) + holdFunction(t - 0.2) * 0.1 + holdFunction(t - 0.4) * 0.01; })); | |
} | |
Sound sounds[4]; | |
Grid<GridState> worldGrid{ worldSize.x, worldSize.y, GridState::Null }; | |
Array<int> highScores = Array<int>(5, 0); | |
int currentScore{ 0 }; | |
}; | |
class Block | |
{ | |
public: | |
Block(const BlockDefinition& blockDefinition) | |
: m_blockDefinition(blockDefinition) | |
, m_angle(Random(m_blockDefinition.angleLimit - 1)) | |
{ | |
calculatePositions(); | |
} | |
bool move(const Grid<GridState>& grid, int x, int y) | |
{ | |
const Point offset{ x, y }; | |
if (checkCollision(grid, m_centerPos + offset, m_angle)) | |
return false; | |
m_centerPos += offset; | |
calculatePositions(); | |
return true; | |
} | |
bool rotate(const Grid<GridState>& grid, bool isClockwize) | |
{ | |
const int defferenceAngle = isClockwize ? 1 : -1; | |
if (checkCollision(grid, m_centerPos, m_angle + (4 + defferenceAngle))) | |
return false; | |
m_angle = (m_angle + (m_blockDefinition.angleLimit + defferenceAngle)) % m_blockDefinition.angleLimit; | |
calculatePositions(); | |
return true; | |
} | |
const std::array<Point, 4>& getPositions() const { return m_positions; } | |
GridState getGridState() const { return m_blockDefinition.gridState; } | |
void setCenterPos(const Point& centerPos) | |
{ | |
m_centerPos = centerPos; | |
calculatePositions(); | |
} | |
const Point& getCenterPos() const { return m_centerPos; } | |
bool checkCollision(const Grid<GridState>& grid, const Point& centerPos, int angle) const | |
{ | |
return AnyOf(m_blockDefinition.figure, [&](const Point& figurePos) | |
{ | |
const Point pos = rotatedPoint(figurePos, angle) + centerPos; | |
return grid[pos.y][pos.x] != GridState::Null; | |
}); | |
} | |
void setAngle(int angle) { m_angle = angle; } | |
int getAngle() const { return m_angle; } | |
private: | |
void calculatePositions() | |
{ | |
for (int i = 0; i < 4; ++i) | |
m_positions[i] = m_centerPos + rotatedPoint(m_blockDefinition.figure[i], m_angle); | |
} | |
Point rotatedPoint(const Point& p, int angle) const | |
{ | |
const std::array<int, 4> rotationValue = { 0, 1, 0, -1 }; | |
const int a = rotationValue[(angle % m_blockDefinition.angleLimit) % 4]; | |
const int b = rotationValue[(angle % m_blockDefinition.angleLimit + 1) % 4]; | |
return{ b * p.x - a * p.y, a * p.x + b * p.y }; | |
} | |
BlockDefinition m_blockDefinition; | |
Point m_centerPos{ 5, 2 }; | |
std::array<Point, 4> m_positions; | |
int m_angle = 0; | |
}; | |
using MyApp = SceneManager<String, CommonData>; | |
class Title : public MyApp::Scene | |
{ | |
public: | |
void update() override | |
{ | |
if (Input::AnyKeyClicked()) | |
changeScene(L"GameMain"); | |
} | |
void draw() const override | |
{ | |
FontAsset(L"H40")(L"テトリス風ゲーム").drawCenter(Window::Center().y - 80); | |
FontAsset(L"H24")(L"PRESS ANY KEY TO START").drawCenter(Window::Center().y + 60); | |
} | |
}; | |
class GameMain : public MyApp::Scene | |
{ | |
public: | |
void init() override | |
{ | |
m_data->currentScore = 0; | |
for (auto p : step(worldSize)) | |
m_data->worldGrid[p.y][p.x] = (p.x == 0 || p.x == worldSize.x - 1 || p.y == worldSize.y - 1) ? GridState::Wall : GridState::Null; | |
m_nextBlocks.clear(); | |
addNewBlocks(); | |
addNewBlocks(); | |
popFrontNewBlock(); | |
} | |
void update() override | |
{ | |
if (Input::KeySpace.clicked) | |
m_isPausing = !m_isPausing; | |
if (m_isPausing) | |
return; | |
if ((Input::KeyRight | Input::KeyLeft).clicked) | |
m_block->move(m_data->worldGrid, Input::KeyRight.clicked - Input::KeyLeft.clicked, 0); | |
if ((Input::KeyZ | Input::KeyX).clicked && m_block->rotate(m_data->worldGrid, Input::KeyX.clicked)) | |
m_data->sounds[2].playMulti(); | |
if (Input::KeyUp.clicked) | |
{ | |
while (m_block->move(m_data->worldGrid, 0, 1)); | |
for (auto& pos : m_block->getPositions()) | |
for (int i = 0; i < 3; ++i) | |
m_particleEffects.emplace_back(((pos + Vec2::One * 0.5 + RandomVec2(Circle(1.0))) * cellSize), 0.1, Random()); | |
putBlock(); | |
} | |
if (Input::KeyC.clicked && (!m_holdedBlock || !m_holdedBlock->checkCollision(m_data->worldGrid, m_block->getCenterPos(), m_holdedBlock->getAngle()))) | |
{ | |
if (m_holdedBlock) | |
m_holdedBlock->setCenterPos(m_block->getCenterPos()); | |
m_holdedBlock.swap(m_block); | |
if (!m_block) | |
popFrontNewBlock(); | |
m_data->sounds[3].playMulti(); | |
} | |
m_predictedBlock = Block(blockDefinitions[m_block->getGridState()]); | |
m_predictedBlock->setCenterPos(m_block->getCenterPos()); | |
m_predictedBlock->setAngle(m_block->getAngle()); | |
while (m_predictedBlock->move(m_data->worldGrid, 0, 1)); | |
++m_timerCount; | |
if (m_timerCount > Max(0, 60 - static_cast<int>(m_data->currentScore * 0.01)) || (Input::KeyDown.clicked || (Input::KeyDown.pressedDuration > 300 && m_timerCount % 6 == 0))) | |
!m_block->move(m_data->worldGrid, 0, 1) ? putBlock() : m_timerCount = 0; | |
if (m_nextBlocks.size() < 7) | |
addNewBlocks(); | |
if (checkGameOver()) | |
changeScene(L"Result"); | |
Erase_if(m_particleEffects, [&](Vec4& e) { return (e.z += 0.01) > 1.0; }); | |
} | |
void draw() const override | |
{ | |
const Point offset(150, -cellSize.y * 0.3); | |
const RectF backRect(offset + cellSize + Vec2::Down * cellSize.y * 0.5, (worldSize - Vec2(2.0, 2.5)) * cellSize); | |
const RoundRect baseRoundRect(backRect.stretched(cellSize), 5.0); | |
baseRoundRect.drawShadow(Vec2::Zero, 20.0); | |
baseRoundRect.draw(Color(0, 183, 227)); | |
baseRoundRect.drawFrame(2.0, 2.0, Palette::White); | |
Graphics2D::SetStencilState(StencilState::Replace); | |
Graphics2D::SetStencilValue(1); | |
backRect.draw(); | |
Graphics2D::SetStencilState(StencilState::Test(StencilFunc::Equal)); | |
backRect.draw(Color(20, 20, 20)); | |
for (auto p : step(Point::One, worldSize - Point(2, 2))) | |
{ | |
if (m_data->worldGrid[p.y][p.x] == GridState::Null) | |
Rect(offset + p * cellSize, cellSize).stretched(-1).draw(gridStateColors[m_data->worldGrid[p.y][p.x]]); | |
else | |
drawBlockRect(offset + p * cellSize, gridStateColors[m_data->worldGrid[p.y][p.x]]); | |
} | |
if (m_predictedBlock) | |
for (const auto& blockPos : m_predictedBlock->getPositions()) | |
Rect(offset + blockPos * cellSize, cellSize).drawFrame(2.0, 0.0, Color(gridStateColors[m_predictedBlock->getGridState()]).setAlpha(128)); | |
for (const auto& blockPos : m_block->getPositions()) | |
drawBlockRect(offset + blockPos * cellSize, gridStateColors[m_block->getGridState()]); | |
Graphics2D::SetStencilState(StencilState::Default); | |
for (auto i : step(3)) | |
for (const auto& blockPos : blockDefinitions[m_nextBlocks[i]].figure) | |
drawBlockRect(offset + Point(300, 90 + i * 100) + blockPos * cellSize, gridStateColors[m_nextBlocks[i]]); | |
if (m_holdedBlock) | |
for (const auto& blockPos : blockDefinitions[m_holdedBlock->getGridState()].figure) | |
drawBlockRect(Point(50, 90) + blockPos * cellSize, gridStateColors[m_holdedBlock->getGridState()]); | |
FontAsset(L"H24")(Pad(m_data->currentScore, { 7, L'0' })).drawCenter((backRect.bl + backRect.br) / 2.0 + Vec2::Down * cellSize.y * 0.5); | |
FontAsset(L"H24")(L"NEXT").drawCenter(Vec2(offset.x + 320, 40)); | |
FontAsset(L"H24")(L"HOLD").drawCenter(Vec2(70, 40)); | |
Graphics2D::SetBlendState(BlendState::Additive); | |
for (const auto& effect : m_particleEffects) | |
Circle(offset + effect.xy() + Vec2::Up * effect.w * 300.0 * (1.0 - Exp(-5 * effect.z)), cellSize.x * 0.3).drawFrame(cellSize.x * 1.0, 0.0, ColorF(1.0, Random(0.15 * Exp(-5 * effect.z)))); | |
Graphics2D::SetBlendState(BlendState::Default); | |
if (m_isPausing) | |
{ | |
Window::ClientRect().draw(ColorF(0.0, 0.3)); | |
FontAsset(L"H24")(L"PAUSE").drawCenter(Window::Center()); | |
} | |
} | |
private: | |
void putBlock() | |
{ | |
for (const auto& blockPos : m_block->getPositions()) | |
m_data->worldGrid[blockPos.y][blockPos.x] = m_block->getGridState(); | |
updateScore(); | |
} | |
void updateScore() | |
{ | |
int count = 0; | |
for (int y = 3; y < worldSize.y - 1; ++y) | |
{ | |
if (AllOf(getGridLine(y, 1, worldSize.x - 2), [&](GridState state) { return state != GridState::Null; })) | |
{ | |
for (int x = 1; x < worldSize.x; ++x) | |
m_particleEffects.emplace_back((Vec2(x, y) + Vec2::One * 0.5) * cellSize, 0.0, 0.0); | |
for (int i = 1; i < y - 1; ++i) | |
for (int x = 1; x < worldSize.x - 1; ++x) | |
m_data->worldGrid[y - i + 1][x] = (i != y - 2) ? m_data->worldGrid[y - i][x] : GridState::Null; | |
++count; | |
} | |
} | |
m_data->currentScore += scoringFormula[count]; | |
popFrontNewBlock(); | |
m_data->sounds[(count ? 0 : 1)].playMulti(); | |
} | |
void addNewBlocks() | |
{ | |
Array<GridState> newBlocks = { GridState::I, GridState::O, GridState::S, GridState::Z, GridState::J, GridState::L, GridState::T }; | |
Shuffle(newBlocks.begin(), newBlocks.end()); | |
m_nextBlocks.insert(m_nextBlocks.end(), newBlocks.begin(), newBlocks.end()); | |
} | |
void popFrontNewBlock() | |
{ | |
m_block = Block(blockDefinitions[static_cast<int>(m_nextBlocks.front())]); | |
m_nextBlocks.erase(m_nextBlocks.begin()); | |
} | |
bool checkGameOver() | |
{ | |
return AnyOf(getGridLine(2, 1, worldSize.x - 2), [&](GridState state) { return state != GridState::Null; }); | |
} | |
const Array<GridState>& getGridLine(int y, int startX, int endX) | |
{ | |
m_line.clear(); | |
for (int x = startX; x <= endX; ++x) | |
m_line.push_back(m_data->worldGrid[y][x]); | |
return m_line; | |
} | |
void drawBlockRect(const Point& pos, const Color& color) const | |
{ | |
const auto rect = RectF(pos, cellSize).stretched(-1); | |
rect.draw(color); | |
rect.stretched(-1, -cellSize.y * 0.4).movedBy(0, -cellSize.y * 0.4).draw(ColorF(1.0, 0.4)); | |
rect.stretched(-1, -cellSize.y * 0.3).movedBy(0, cellSize.y * 0.3).draw(ColorF(1.0, 0.2)); | |
rect.drawFrame(1.0, 1.0, ColorF(0.0, 0.2)); | |
} | |
Optional<Block> m_block, m_holdedBlock, m_predictedBlock; | |
Array<GridState> m_line, m_nextBlocks; | |
Array<Vec4> m_particleEffects; | |
int m_timerCount = 0; | |
bool m_isPausing = false; | |
}; | |
class Result : public MyApp::Scene | |
{ | |
public: | |
void init() override | |
{ | |
m_data->highScores.push_back(m_data->currentScore); | |
std::sort(m_data->highScores.begin(), m_data->highScores.end(), std::greater<int>()); | |
m_data->highScores.pop_back(); | |
m_timer.start(); | |
} | |
void update() override | |
{ | |
if (Input::AnyKeyClicked()) | |
changeScene(L"GameMain"); | |
} | |
void draw() const override | |
{ | |
const double offsetY = static_cast<double>(Window::Height() - 100) / m_data->highScores.size(); | |
const double startY = Window::Height() * 0.5 - (m_data->highScores.size() / 2.0) * offsetY; | |
FontAsset(L"H24")(L"RESULTS").drawCenter(Vec2(Window::Width()* 0.5, startY)); | |
for (auto i : step(m_data->highScores.size())) | |
{ | |
const Vec2 pos = Vec2(Window::Width() * 0.5 * (1.0 - Exp(-0.007 * (m_timer.elapsed().count() - i * 100.0))), startY + offsetY * (i + 1)); | |
const auto backRoundRect = RoundRect(RectF(Window::Width() * 0.8, offsetY * 0.9).setCenter(pos), 10.0); | |
backRoundRect.draw((m_data->currentScore == m_data->highScores[i] && m_data->currentScore != 0) ? Palette::White : Color(0, 183, 227)); | |
backRoundRect.drawFrame(2.0, 2.0); | |
FontAsset(L"H24")(i + 1).drawCenter(pos + Vec2::Left * 200.0); | |
FontAsset(L"H24")(Pad(m_data->highScores[i], { 7, L'0' })).drawCenter(pos + Vec2::Right * 20.0); | |
} | |
} | |
Stopwatch m_timer; | |
}; | |
void Main() | |
{ | |
Window::SetTitle(L"落ち物ゲーム Z,X:回転 C:ホールド 左右下:移動 上:ドロップ"); | |
Graphics::SetBackground(Color(0, 183, 227)); | |
FontAsset::Register(L"H40", 40, Typeface::Heavy, FontStyle::Outline); | |
FontAsset::Register(L"H24", 24, Typeface::Heavy, FontStyle::Outline); | |
MyApp sceneManager; | |
sceneManager.add<Title>(L"Title"); | |
sceneManager.add<GameMain>(L"GameMain"); | |
sceneManager.add<Result>(L"Result"); | |
while (System::Update()) | |
{ | |
for (auto& s : step((Window::Size() / 50) + Point::One * 2)) | |
if ((s.x + s.y) % 2) | |
RectF(50).setCenter(50 * s).moveBy(Vec2(50, 50) * (System::FrameCount() % 120 / 120.0 - 1.0)).draw(AlphaF(0.1)); | |
sceneManager.updateAndDraw(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment