Hangman With Nothing

2019 年 10 月|Ludum Dare 45:Start With Nothing



项目信息

  • 编程语言: C++
  • 工具: Visual Studio, OpenGL 3
  • itch.io: 链接
  • 下载: 链接

玩法

本作的玩法与传统 Hangman(猜词游戏)相似。不同之处在于,解谜时需要使用从提示中击落的文字方块;如果方块耗尽,游戏就会结束。


操作方式

  • 方向键—移动
  • 空格键—击碎文字
  • A~Z—尝试字母

Game.cpp

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();
}


            

项目复盘


做得好的地方

  • 在两天内成功完成了一款简单有趣的游戏。

需要改进的地方

  • 直到比赛最后 24 小时才真正确定游戏创意。

我的收获

  • 围绕新鲜创意开发游戏非常有趣。