2019 年 10 月|Ludum Dare 45:Start With Nothing
本作的玩法与传统 Hangman(猜词游戏)相似。不同之处在于,解谜时需要使用从提示中击落的文字方块;如果方块耗尽,游戏就会结束。
操作方式
该 Game 代码负责游戏的核心循环,几乎所有与游戏有关的逻辑都写在这里。
#include "Engine/Math/MathUtils.hpp"
#include "Engine/Core/ErrorWarningAssert.hpp"
#include "Engine/Input/InputSystem.hpp"
#include "Engine/Core/Time.hpp"
#include "Game/Game.hpp"
#include "Game/GameCommon.hpp"
#include "Game/App.hpp"
#include "Game/TextHelper.hpp"
#include "Engine/Math/AABB2.hpp"
#include "Game/Dot.hpp"
#include "Game/Cursor.hpp"
TextHelper* m_textHelper = nullptr;
const char* puzzleHints[] = {
"ARROWS TO MOVE THE CURSOR\nSPACE TO BREAK TEXTS\nF1 TO RESTART",
"BINARY",
"MAKE AMERICA GREAT AGAIN",
"LAW OF UNIVERSAL GRAVITATION",
"THREAD",
"PEN APPLE",
"Color 0",
"MINIONS",
"SHAPE OF CURSOR",
"FIRST ELECTRONIC\nGENERAL-PURPOSE COMPUTER",
"DRAGON QUEST",
"SHIGERU MIYAMOTO",
"BLADE RUNNER",
"STEINS;GATE",
"GAMES WITH TEXT BASED STORIES",
"YANKEES",
"ARTIFICIAL INTELLIGENCE",
"HIGHEST MOUNTAIN",
"FIRST MARVEL SUPERHERO",
"START WITH",
};
const char* puzzleWords[] = {
"TUTORIAL",
"TWO",
"TRUMP",
"NEWTON",
"NEEDLE",
"PINEAPPLE",
"BLACK",
"BANANA",
"SQUARE",
"ENIAC",
"SLIME",
"NINTENDO",
"CYBERPUNK",
"TIME MACHINE",
"VISUAL NOVEL",
"NEW YORK",
"NEURAL NETWORK",
"EVEREST",
"HUMAN TORCH",
"NOTHING",
};
const int levelNum = 20;
Game::Game()
{
m_textHelper = new TextHelper();
m_textHelper->StartUp();
m_textHelper->m_game = this;
m_startGame = false;
m_restartGame = false;
m_worldCamera.SetOrthoView(Vec2(0, 0), Vec2(WORLD_SIZE_X, WORLD_SIZE_Y));
m_uiCamera.SetOrthoView(Vec2(0, 0), Vec2(WORLD_SIZE_X, WORLD_SIZE_Y));
//m_currentLevel = 19;
m_cursor = new Cursor(this, Vec2(WORLD_CENTER_X, WORLD_CENTER_Y - 24));
GenerateNewPuzzle();
}
Game::~Game()
{
}
const void Game::BeginFrame()
{
}
const void Game::Update( float deltaSeconds )
{
static float xShift = 0;
static float yShift = 0;
if (m_screenShakeIntensity > 0)
{
xShift = m_rng->GetRandomFloatInRange(0, m_screenShakeIntensity);
yShift = m_rng->GetRandomFloatInRange(0, m_screenShakeIntensity);
m_screenShakeIntensity -= m_screenShakeDecreaseSpeed * deltaSeconds;
}
m_worldCamera.SetOrthoView(Vec2(0, 0), Vec2(WORLD_SIZE_X, WORLD_SIZE_Y));
m_worldCamera.Translate(Vec2(xShift, yShift));
UpdateFromKeyboard(deltaSeconds);
UpdateFromJoystick();
//TextAnimationTest();
UpdateGameState();
//CreateStoryBoard();
//CheckNextLevel();
UpdateDots(deltaSeconds);
m_gameLastTime = m_gameTime;
m_gameTime += deltaSeconds;
}
const void Game::UpdateDots(float deltaSeconds) const
{
for (int i = 0; i < m_deadDots.size(); i++)
{
m_deadDots[i]->Update(deltaSeconds);
}
for (int i = 0; i < m_dots.size(); i++)
{
m_dots[i]->Update(deltaSeconds);
}
}
const void Game::Render() const
{
Rgba8 black = Rgba8(0, 0, 0, 255);
g_theRenderer->ClearScreen(black);
g_theRenderer->BeginCamera(m_worldCamera);
RenderWorld();
g_theRenderer->EndCamera(m_worldCamera);
g_theRenderer->BeginCamera(m_uiCamera);
//RenderUI();
g_theRenderer->EndCamera(m_uiCamera);
if (g_theApp->m_isDebugMode)
{
DebugRender();
}
}
const void Game::CleanUp()
{
for (int i = 0; i < m_dots.size(); i++)
{
delete (m_dots[i]);
}
m_dots.clear();
for (int i = 0; i < m_deadDots.size(); i++)
{
delete (m_deadDots[i]);
}
m_deadDots.clear();
m_numDeadDots = 0;
for (int i = 0; i < 512; i++)
m_wrongText[i] = '\0';
m_wrongTextNum = 0;
}
const void Game::GameOverCleanUp()
{
for (int i = 0; i < m_dots.size(); i++)
{
if (!m_dots[i]->HasPhysics()) {
m_dots[i]->m_isAutoUpdate = true; m_dots[i]->Die();
}
}
for (int i = 0; i < 512; i++)
m_wrongText[i] = '\0';
m_wrongTextNum = 0;
}
const void Game::RenderWorld() const
{
RenderDots();
RenderUI();
RenderCursor();
g_theRenderer->DrawVertexArray(m_puzzleVertexes);
}
const void Game::RenderDots() const
{
static std::vector renderVertexes;
renderVertexes.clear();
for (int i = 0; i < m_dots.size(); i++)
{
m_dots[i]->AddForRender(renderVertexes);
}
for (int i = 0; i < m_deadDots.size(); i++)
{
m_deadDots[i]->AddForRender(renderVertexes);
}
g_theRenderer->DrawVertexArray(renderVertexes);
}
const void Game::RenderUI() const
{
if (m_gameState != GS_GAMEOVER)
m_textHelper->DrawString((const char*)m_wrongText, 1.f, Vec2(WORLD_CENTER_X, 50), Vec2(.5f, .5f), Rgba8(255, 0, 0, 255), 4);
}
const void Game::DebugRender() const
{
//if (m_gameState != GS_PLAYING) return;
char buff[128];
sprintf_s(buff, 128, "Dot Num %d", m_dots.size() + m_deadDots.size());
m_textHelper->DrawString((const char*)(buff), .5f, Vec2(20, 150), Vec2(0, 1), Rgba8(0, 255, 0, 255));
}
const void Game::EndFrame()
{
CleanGarbage();
}
const void Game::UpdateGameState()
{
if (CheckWordFinished() && m_gameState == GS_GUESSING) {
m_gameTime = 0;
m_gameLastTime = 0;
m_gameState = GS_FINISHEDLEVEL;
}
if (m_gameState == GS_FINISHEDLEVEL && m_gameTime > 1) {
if (m_currentLevel < levelNum - 1)
{
m_currentLevel++;
GenerateNewPuzzle();
}
else
{
CleanUp();
m_gameState = GS_GAMEOVER;
m_puzzleVertexes.clear();
RenderGameOverContext();
}
}
}
const void Game::RenderGameOverContext()
{
char buff[128];
m_textHelper->CreateDotsFromString(m_dots, "THANKS FOR YOUR PLAYING!", 1.f, Vec2(WORLD_CENTER_X, 150), Vec2(.5f, .5f), GetRandomColor(), false);
m_textHelper->CreateDotsFromString(m_dots, "Result", .8f, Vec2(WORLD_CENTER_X - 60, 80), Vec2(.5f, .5f), GetRandomColor(), false);
sprintf_s(buff, 128, "Restart %d times\nFail trials %d times", m_gameRestartNum, m_gameWrongCharNum);
m_textHelper->CreateDotsFromString(m_dots, (const char*)buff, .7f, Vec2(WORLD_CENTER_X, 55), Vec2(.5f, .5f), GetRandomColor(), false);
m_textHelper->CreateDotsFromString(m_dots, "Created by Yao Shen 2019", .5f, Vec2(WORLD_CENTER_X, 10), Vec2(.5f, .5f), GetRandomColor(), false);
}
const void Game::CleanGarbage()
{
std::vector::iterator it = m_dots.begin();
while (it != m_dots.end()) {
if ((*it)->IsGarbage()) {
it = m_dots.erase(it);
}
else ++it;
}
it = m_deadDots.begin();
while (it != m_deadDots.end()) {
if ((*it)->IsGarbage()) {
it = m_deadDots.erase(it);
}
else ++it;
}
}
const void Game::UpdateFromKeyboard(float deltaSeconds)
{
if (g_theInput->WasKeyJustPressed(KEY_LEFTARROW))
{
if (m_cursor->GetPosition().x - m_cursor->m_cursorSize / 2 > 0)
m_cursor->Translate(Vec2(-m_cursor->m_cursorSize / 2, 0));
}
if (g_theInput->WasKeyJustPressed(KEY_RIGHTARROW))
{
if (m_cursor->GetPosition().x + m_cursor->m_cursorSize / 2 < WORLD_SIZE_X)
m_cursor->Translate(Vec2(m_cursor->m_cursorSize / 2, 0));
}
if (g_theInput->WasKeyJustPressed(KEY_UPARROW))
{
if (m_cursor->GetPosition().y + m_cursor->m_cursorSize / 2 < WORLD_SIZE_Y)
m_cursor->Translate(Vec2(0, m_cursor->m_cursorSize / 2));
}
if (g_theInput->WasKeyJustPressed(KEY_DOWNARROW))
{
if (m_cursor->GetPosition().y - m_cursor->m_cursorSize / 2 > 0)
m_cursor->Translate(Vec2(0, -m_cursor->m_cursorSize / 2));
}
if (!g_theInput->GetKeyState(KEY_LEFTARROW).IsPressed()) { m_leftDownTime = 0; m_leftLongPress = false; }
if (!g_theInput->GetKeyState(KEY_RIGHTARROW).IsPressed()){ m_rightDownTime = 0; m_rightLongPress = false;}
if (!g_theInput->GetKeyState(KEY_UPARROW).IsPressed()) { m_upDownTime = 0; m_upLongPress = false; }
if (!g_theInput->GetKeyState(KEY_DOWNARROW).IsPressed()) { m_downDownTime = 0; m_downLongPress = false; }
if (g_theInput->GetKeyState(KEY_LEFTARROW).IsPressed()) {
m_leftDownTime += deltaSeconds;
if (m_leftDownTime > 0.5f) m_leftLongPress = true;
if (m_leftDownTime > 0.1f && m_leftLongPress) {
if (m_cursor->GetPosition().x - m_cursor->m_cursorSize / 2 > 0)
m_cursor->Translate(Vec2(-m_cursor->m_cursorSize / 2, 0));
m_leftDownTime = 0;
}
}
if (g_theInput->GetKeyState(KEY_RIGHTARROW).IsPressed()) {
m_rightDownTime += deltaSeconds;
if (m_rightDownTime > 0.5f) m_rightLongPress = true;
if (m_rightDownTime > 0.1f && m_rightLongPress) {
if (m_cursor->GetPosition().x + m_cursor->m_cursorSize / 2 < WORLD_SIZE_X)
m_cursor->Translate(Vec2(m_cursor->m_cursorSize / 2, 0));
m_rightDownTime = 0;
}
}
if (g_theInput->GetKeyState(KEY_UPARROW).IsPressed()) {
m_upDownTime += deltaSeconds;
if (m_upDownTime > 0.5f) m_upLongPress = true;
if (m_upDownTime > 0.1f && m_upLongPress) {
if (m_cursor->GetPosition().y + m_cursor->m_cursorSize / 2 < WORLD_SIZE_Y)
m_cursor->Translate(Vec2(0, m_cursor->m_cursorSize / 2));
m_upDownTime = 0;
}
}
if (g_theInput->GetKeyState(KEY_DOWNARROW).IsPressed()) {
m_downDownTime += deltaSeconds;
if (m_downDownTime > 0.5f) m_downLongPress = true;
if (m_downDownTime > 0.1f && m_downLongPress) {
if (m_cursor->GetPosition().y - m_cursor->m_cursorSize / 2 > 0)
m_cursor->Translate(Vec2(0, -m_cursor->m_cursorSize / 2));
m_downDownTime = 0;
}
}
if (g_theInput->WasKeyJustPressed(KEY_SPACEBAR))
{
BreakBricks();
if (m_gameState == GS_GAMEOVER) {
GameOverCleanUp();
RenderGameOverContext();
}
}
//for (unsigned char ch = '0'; ch <= '9'; ch++) {
// if (g_theInput->WasKeyJustPressed(ch) && m_deadDots.size() > m_textHelper->GetPointNum(ch)) {
// m_playerText[m_playerTextNum] = ch;
// m_playerTextNum++;
// RemoveDeadDotTile(m_textHelper->GetPointNum(ch));
// }
//}
for (unsigned char ch = 'A'; ch <= 'Z'; ch++) {
if (g_theInput->WasKeyJustPressed(ch)) {
ScreenShake(.4f, .2f);
}
if (g_theInput->WasKeyJustPressed(ch) && m_deadDots.size() > m_textHelper->GetPointNum(ch)) {
if (CheckHitAnswer(ch) != -1) {
m_textHelper->CreateDotsFromChar(m_dots, ch, 1.f, charPosition[CheckHitAnswer(ch)], Rgba8(255, 255, 128, 255), true);
isCharHit[CheckHitAnswer(ch)] = true;
} else {
if (m_wrongTextNum == 9 * 4 - 1) {
return;
}
if (((m_wrongTextNum + 1) % 9) == 0) {
m_wrongText[m_wrongTextNum] = '\n';
m_wrongTextNum++;
}
m_wrongText[m_wrongTextNum] = ch;
m_wrongTextNum++;
m_gameWrongCharNum++;
}
RemoveDeadDotTile(m_textHelper->GetPointNum(ch));
}
}
if (g_theInput->GetKeyState(KEY_F1).WasJustPressed())
{
if (m_gameState == GS_GUESSING) {
RestartCurrentLevel();
m_gameRestartNum++;
}
if (m_gameState == GS_GAMEOVER) {
CleanUp();
RenderGameOverContext();
}
}
//if (g_theInput->WasKeyJustPressed(KEY_BACKSPACE)) {
// if (m_playerTextNum > 0) {
// unsigned char ch = m_playerText[m_playerTextNum - 1];
// m_playerText[m_playerTextNum-1] = '\0';
// m_playerTextNum--;
// for (int i = 0; i < m_textHelper->GetPointNum(ch); i++) {
// AddDeadDotTile();
// }
// }
//}
if (g_theInput->WasKeyJustPressed(KEY_ESC)) {
g_theApp->m_isQuitting = true;
}
}
const void Game::UpdateFromJoystick()
{
// ONLY TAKE COMMANDS FROM THE FIRST CONTROLLER!!
if (!g_theInput->GetXboxController(0).IsConnected())
return;
}
const void Game::BreakBricks()
{
int m_breaknum = 0;
for (int i = 0; i < m_dots.size(); i++)
{
if (m_dots[i]->HasPhysics() || m_dots[i]->m_isDeadDot) continue;
AABB2 dotBox = m_dots[i]->GetAABB2();
AABB2 cursorArea = m_cursor->GetAABB2();
if (DoAABBsOverlap2D(dotBox, cursorArea)) {
float randomAngle = m_rng->GetRandomFloatInRange(45, 135);
Vec2 randomDir = Vec2::MakeFromPolarDegrees(randomAngle, m_rng->GetRandomFloatInRange(20, 50));
m_dots[i]->SetPhysics(randomDir);
m_breaknum++;
}
}
if (m_breaknum != 0)
{
ScreenShake(2.f, .5f);
m_breakBricks = true;
}
}
const void Game::AddDeadDotTile(const Rgba8& color)
{
float pos_x = m_numDeadDots % 256;
float pos_y = m_numDeadDots / 256;
Dot* newDOT = new Dot(this, Vec2(pos_x, pos_y), 1.f, color);
newDOT->m_isDeadDot = true;
m_deadDots.push_back(newDOT);
m_numDeadDots++;
}
const char* Game::GenerateNewWord()
{
return puzzleWords[m_currentLevel];
}
const void Game::GenerateNewPuzzle()
{
CleanUp();
m_gameState = GS_GUESSING;
m_textHelper->CreateDotsFromString(m_dots, puzzleHints[m_currentLevel], 1.f, Vec2(WORLD_CENTER_X, WORLD_SIZE_Y - 40), Vec2(.5f, .5f), Rgba8(255, 255, 255, 255), false);
char buff[128];
sprintf_s(buff, 128, "%d", m_currentLevel + 1);
m_textHelper->CreateDotsFromString(m_dots, buff, 1.f, Vec2(WORLD_SIZE_X - 8, WORLD_SIZE_Y - 10), Vec2(1.f, .5f), Rgba8(255, 255, 255, 255), false);
m_puzzleLength = 0;
const char* word = GenerateNewWord();
m_currentWord = word;
for (int i = 0; word[i] != '\0'; i++)
m_puzzleLength++;
float phyLength = m_puzzleLength * 8 + (m_puzzleLength - 1) * 4;
float startX = WORLD_CENTER_X - phyLength / 2;
float pY = WORLD_CENTER_Y + 10;
std::vector vertexes;
charPosition.clear();
isCharHit.clear();
for (int i = 0; i < m_puzzleLength; i++) {
if (word[i] != ' ')
AppendVertsForLine2D(vertexes, Vec2(startX, pY), Vec2(startX + 8, pY), Rgba8(255, 255, 255, 255), .5f);
charPosition.push_back(Vec2(startX - 2.5f, pY));
isCharHit.push_back(false);
startX += 12;
}
m_puzzleVertexes = vertexes;
}
const int Game::CheckHitAnswer(unsigned char ch)
{
for (int i = 0; i < m_puzzleLength; i++)
if (ch == m_currentWord[i] && !isCharHit[i]) return i;
return -1;
}
Rgba8 Game::GetRandomColor()
{
int r = m_rng->GetRandomIntInRange(0, 255);
int g = m_rng->GetRandomIntInRange(0, 255);
int b = m_rng->GetRandomIntInRange(0, 255);
while (r + g + b < 400)
{
r = m_rng->GetRandomIntInRange(0, 255);
g = m_rng->GetRandomIntInRange(0, 255);
b = m_rng->GetRandomIntInRange(0, 255);
}
return Rgba8(r, g, b, 255);
}
const void Game::RemoveDeadDotTile(int num)
{
for (int i = 0; i < num; i++)
{
Dot* dot = m_deadDots[m_deadDots.size() - 1];
dot->m_isAutoUpdate = true;
dot->Die();
m_deadDots.pop_back();
}
m_numDeadDots -= num;
}
const void Game::CleanAliveDots()
{
for (int i = 0; i < m_dots.size(); i++)
{
if (!m_dots[i]->IsGarbage()) m_dots[i]->m_isAutoUpdate = true;
if (!m_dots[i]->HasPhysics())
m_dots[i]->Die();
}
}
const void Game::ScreenShake(float intensity, float shakeTime)
{
m_screenShakeIntensity = intensity;
m_screenShakeDecreaseSpeed = intensity / shakeTime;
}
const void Game::CreateTextAnimation(const char* text, float startTime, float interval, const Vec2& pivot, GameState atGameState)
{
if (m_gameState != atGameState) return;
if (m_gameTime < startTime) return;
char buff[512];
int i = 0;
while (i == 0 || text[i-1] != '\0')
{
if (m_gameLastTime < (startTime + interval * i) && m_gameTime >= (startTime + interval * i)) {
strncpy_s(buff, text, i);
CleanAliveDots();
m_textHelper->CreateDotsFromString(m_dots, buff, 1.f, Vec2(WORLD_CENTER_X, WORLD_CENTER_Y), pivot, Rgba8(127, 127, 127, 255), false);
m_breakBricks = false;
}
i++;
}
}
const void Game::CreateStoryBoard()
{
//CreateTextAnimation("Hello", 0.f, .15f, Vec2(.5f, .5f), GS_WELCOME);
//CreateTextAnimation("I'm Artificial Intelligence\n Stella, I've been waiting for you so long.", 2.f, .15f, Vec2(.5f, .5f), GS_WELCOME);
//CreateTextAnimation("Ok, this is the game for\nLudam Dare 45. I agree I\nmade a mistake by making\ntexts breakable.", .5f, .1f, Vec2(.5f, .5f), GS_S1);
//CreateTextAnimation("Please, stop breaking my\ntexts. I know I'm a bad\nwritter though...", .5f, .1f, Vec2(.5f, .5f), GS_S2);
//CreateTextAnimation("Nothing more, enjoy breaking the texts here.", .5f, .1f, Vec2(.5f, .5f), GS_S3);
//char buff[512];
//for (int i = 0; i < 256; i++) {
// int randType = m_rng->GetRandomIntInRange(0, 2);
// if (randType == 0)
// buff[i] = '0' + m_rng->GetRandomIntInRange(0, 9);
// else if (randType == 1)
// buff[i] = 'A' + m_rng->GetRandomIntInRange(0, 25);
// else
// buff[i] = 'a' + m_rng->GetRandomIntInRange(0, 25);
// if ((i+1) % 32 == 0) buff[i] = '\n';
//}
//buff[128] = '/0';
//CreateTextAnimation((const char*)buff, .5f, .1f, Vec2(.5f, .5f), GS_S4);
}
bool Game::CheckWordFinished()
{
for (int i = 0; i < m_puzzleLength; i++) {
if (!isCharHit[i] && m_currentWord[i] != ' ') return false;
}
return true;
}
void Game::RenderCursor() const
{
m_cursor->Render();
}
void Game::RestartCurrentLevel()
{
GenerateNewPuzzle();
}