From 871943d6a90bdb92b3cc495d4d927199611f8c6b Mon Sep 17 00:00:00 2001 From: Kelly Rauchenberger Date: Thu, 4 Feb 2021 20:45:18 -0500 Subject: Added text boxes Text now reveals itself and scrolls! Yay! It even plays speaker beeps. TODO: the arror indicating an A press is needed. Bullets on lines that need bullets. The header that says who the speaker is, if relevant. --- CMakeLists.txt | 1 + res/font.png | Bin 0 -> 2445 bytes res/font.txt | 96 +++++++++++++++++++++++++++++ res/speaking_boy.wav | Bin 0 -> 8460 bytes res/speaking_girl.wav | Bin 0 -> 7672 bytes res/speaking_man.wav | Bin 0 -> 9536 bytes res/speaking_nonhuman.wav | Bin 0 -> 12316 bytes res/speaking_woman.wav | Bin 0 -> 8384 bytes src/consts.h | 2 + src/font.cpp | 63 +++++++++++++++++++ src/font.h | 33 ++++++++++ src/game.h | 8 +++ src/input_system.cpp | 9 ++- src/main.cpp | 3 +- src/message_system.cpp | 153 ++++++++++++++++++++++++++++++++++++++++++++++ src/message_system.h | 28 +++++++++ src/renderer.cpp | 71 +++++++++++++++++++++ src/renderer.h | 11 ++++ 18 files changed, 474 insertions(+), 4 deletions(-) create mode 100644 res/font.png create mode 100644 res/font.txt create mode 100644 res/speaking_boy.wav create mode 100644 res/speaking_girl.wav create mode 100644 res/speaking_man.wav create mode 100644 res/speaking_nonhuman.wav create mode 100644 res/speaking_woman.wav create mode 100644 src/font.cpp create mode 100644 src/font.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 36acd0b..5626362 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(tanetane src/mixer.cpp src/game.cpp src/map.cpp + src/font.cpp src/transform_system.cpp src/camera_system.cpp src/animation_system.cpp diff --git a/res/font.png b/res/font.png new file mode 100644 index 0000000..aacca94 Binary files /dev/null and b/res/font.png differ diff --git a/res/font.txt b/res/font.txt new file mode 100644 index 0000000..a089e8c --- /dev/null +++ b/res/font.txt @@ -0,0 +1,96 @@ +../res/font.png +90 characters +10 characters per row +12x12 tile size +8 baseline + +! 1 +" 3 +# 5 +$ 5 +% 9 +& 7 +' 2 +( 3 +) 3 +* 3 ++ 5 +, 2 +- 3 +. 1 +/ 4 +0 4 +1 2 +2 4 +3 4 +4 5 +5 4 +6 4 +7 4 +8 4 +9 4 +: 1 +; 2 +< 4 += 5 +> 4 +? 4 +@ 7 +A 6 +B 5 +C 5 +D 5 +E 4 +F 4 +G 5 +H 5 +I 1 +J 4 +K 5 +L 4 +M 7 +N 5 +O 5 +P 5 +Q 5 +R 5 +S 5 +T 5 +U 5 +V 6 +W 7 +X 5 +Y 5 +Z 4 +[ 2 +~ 6 +] 2 +^ 3 +_ 2 +` 0 +a 5 +b 4 +c 4 +d 4 +e 4 +f 3 +g 4 +h 4 +i 1 +j 2 +k 4 +l 1 +m 7 +n 4 +o 4 +p 4 +q 4 +r 3 +s 4 +t 3 +u 4 +v 5 +w 7 +x 4 +y 4 +z 4 \ No newline at end of file diff --git a/res/speaking_boy.wav b/res/speaking_boy.wav new file mode 100644 index 0000000..78c5a95 Binary files /dev/null and b/res/speaking_boy.wav differ diff --git a/res/speaking_girl.wav b/res/speaking_girl.wav new file mode 100644 index 0000000..67365d3 Binary files /dev/null and b/res/speaking_girl.wav differ diff --git a/res/speaking_man.wav b/res/speaking_man.wav new file mode 100644 index 0000000..0a0f56d Binary files /dev/null and b/res/speaking_man.wav differ diff --git a/res/speaking_nonhuman.wav b/res/speaking_nonhuman.wav new file mode 100644 index 0000000..098ab53 Binary files /dev/null and b/res/speaking_nonhuman.wav differ diff --git a/res/speaking_woman.wav b/res/speaking_woman.wav new file mode 100644 index 0000000..6ae42bd Binary files /dev/null and b/res/speaking_woman.wav differ diff --git a/src/consts.h b/src/consts.h index 5ef2c21..12b9e8f 100644 --- a/src/consts.h +++ b/src/consts.h @@ -10,4 +10,6 @@ const int CANVAS_HEIGHT = 160; const int MOVEMENT_SPEED = 2; const int PARTY_FRAME_DELAY = 10;// / MOVEMENT_SPEED; +const int MESSAGE_TEXT_WIDTH = 196; + #endif /* end of include guard: CONSTS_H_9561E49C */ diff --git a/src/font.cpp b/src/font.cpp new file mode 100644 index 0000000..cfaad24 --- /dev/null +++ b/src/font.cpp @@ -0,0 +1,63 @@ +#include "font.h" +#include +#include +#include "renderer.h" + +inline int charToIndex(char ch) { + return static_cast(ch) - static_cast('!'); +} + +Font::Font(std::string_view filename, Renderer& renderer) { + std::ifstream fontfile(filename.data()); + if (!fontfile.is_open()) { + throw std::invalid_argument("Can't find font file: " + std::string(filename)); + } + + std::string imagefilename; + fontfile >> imagefilename; + textureId_ = renderer.loadImageFromFile(imagefilename); + + std::string line; + char ch; + + int numChars; + fontfile >> numChars; + std::getline(fontfile, line); // characters + + fontfile >> columns_; + std::getline(fontfile, line); // characters per row + + fontfile >> tileSize_.x(); + fontfile >> ch; // x + fontfile >> tileSize_.y(); + std::getline(fontfile, line); // tile size + + std::getline(fontfile, line); // ignore the baseline location + std::getline(fontfile, line); // blank + + for (int i=0; i> ch; // the character + + int width; + fontfile >> width; + + widths_.push_back(width); + } +} + +vec2i Font::getCharacterLocation(char ch) const { + int index = charToIndex(ch); + return (vec2i { index % columns_, index / columns_ }) * tileSize_; +} + +vec2i Font::getCharacterSize(char ch) const { + int index = charToIndex(ch); + return { widths_.at(index), tileSize_.h() }; +} + +int Font::getCharacterWidth(char ch) const { + if (ch == ' ') { + return 3; + } + return widths_.at(charToIndex(ch)); +} diff --git a/src/font.h b/src/font.h new file mode 100644 index 0000000..fd24830 --- /dev/null +++ b/src/font.h @@ -0,0 +1,33 @@ +#ifndef FONT_H_C80183A7 +#define FONT_H_C80183A7 + +#include +#include +#include "vector.h" + +class Renderer; + +class Font { +public: + + Font(std::string_view filename, Renderer& renderer); + + int getTextureId() const { return textureId_; } + + vec2i getCharacterLocation(char ch) const; + + vec2i getCharacterSize(char ch) const; + + int getCharacterWidth(char ch) const; + + int getCharacterHeight() const { return tileSize_.h(); } + +private: + + int textureId_; + int columns_; + vec2i tileSize_; + std::vector widths_; +}; + +#endif /* end of include guard: FONT_H_C80183A7 */ diff --git a/src/game.h b/src/game.h index 36398bc..2f1149d 100644 --- a/src/game.h +++ b/src/game.h @@ -11,10 +11,15 @@ #include "consts.h" #include "system.h" #include "mixer.h" +#include "font.h" + +class Renderer; class Game { public: + explicit Game(Renderer& renderer) : font_("../res/font.txt", renderer) {} + Mixer& getMixer() { return mixer_; } bool shouldQuit() const { return shouldQuit_; } @@ -68,6 +73,8 @@ public: const Map& getMap() const { return *map_; } + const Font& getFont() const { return font_; } + private: Mixer mixer_; @@ -79,6 +86,7 @@ private: std::vector spriteIds_; std::vector sprites_; std::unique_ptr map_; + Font font_; }; #endif /* end of include guard: GAME_H_E6F1396E */ diff --git a/src/input_system.cpp b/src/input_system.cpp index 7bd605c..a0acc5e 100644 --- a/src/input_system.cpp +++ b/src/input_system.cpp @@ -27,11 +27,16 @@ void InputSystem::tick(double dt) { } } else if (e.key.keysym.sym == SDLK_a) { // TODO: Remove this, it's just for testing. - if (game_.getSystem().getCutsceneBarsProgress() == 1.0) { + /*if (game_.getSystem().getCutsceneBarsProgress() == 1.0) { game_.getSystem().hideCutsceneBars(); } else { game_.getSystem().displayCutsceneBars(); - } + }*/ + //game_.getSystem().displayMessage("Some people always try to avoid fighting when there are enemies around. You know the type, right? They use the dash ability to zoom right by. I guess you could say they're followers of \"peace at any price\".", SpeakerType::Woman); + game_.getSystem().displayMessage("Go to sleep.\nIn the absolute darkness.\nDon't do aaaanything.\nDon't see anyone.\nJust sleep.\nIt'll be oh, so much fun......!\nOhohohoho!", SpeakerType::Woman); + } else if (e.key.keysym.sym == SDLK_b) { + // TODO: Remove this, it's just for testing. + game_.getSystem().advanceText(); } } else if (e.type == SDL_KEYUP && (e.key.keysym.sym == SDLK_LSHIFT || e.key.keysym.sym == SDLK_RSHIFT)) { for (int spriteId : game_.getSprites()) { diff --git a/src/main.cpp b/src/main.cpp index 6674c0c..935fc78 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,7 +4,6 @@ #include "game.h" #include "timer.h" #include "map.h" -#include "mixer.h" #include "transform_system.h" #include "camera_system.h" #include "animation_system.h" @@ -13,7 +12,7 @@ #include "message_system.h" void loop(Renderer& renderer) { - Game game; + Game game(renderer); game.emplaceSystem(); game.emplaceSystem(); game.emplaceSystem(); diff --git a/src/message_system.cpp b/src/message_system.cpp index 71e8a5a..5abb4b3 100644 --- a/src/message_system.cpp +++ b/src/message_system.cpp @@ -1,4 +1,9 @@ #include "message_system.h" +#include "game.h" +#include "util.h" + +const int CHARS_TO_REVEAL = 1; +const int CHARS_PER_BEEP = 10; void MessageSystem::tick(double dt) { if (barsState_ == BarsState::Opening || barsState_ == BarsState::Closing) { @@ -11,6 +16,53 @@ void MessageSystem::tick(double dt) { barsState_ = BarsState::Closed; } } + } else if (barsState_ == BarsState::Open) { + if (!linesToShow_.empty()) { + textAdvTimer_.accumulate(dt); + while (textAdvTimer_.step()) { + // Try to advance text on the first line that isn't totally revealed yet. + bool advancedChars = false; + for (MessageLine& line : linesToShow_) { + if (line.charsRevealed < line.text.size()) { + // Every so often play a beep. + if (line.charsRevealed % CHARS_PER_BEEP == 0) { + if (speaker_ == SpeakerType::Man) { + game_.getMixer().playSound("../res/speaking_man.wav"); + } else if (speaker_ == SpeakerType::Woman) { + game_.getMixer().playSound("../res/speaking_woman.wav"); + } else if (speaker_ == SpeakerType::Boy) { + game_.getMixer().playSound("../res/speaking_boy.wav"); + } else if (speaker_ == SpeakerType::Girl) { + game_.getMixer().playSound("../res/speaking_girl.wav"); + } else if (speaker_ == SpeakerType::Nonhuman) { + game_.getMixer().playSound("../res/speaking_nonhuman.wav"); + } + } + + line.charsRevealed += CHARS_TO_REVEAL; + if (line.charsRevealed > line.text.size()) { + line.charsRevealed = line.text.size(); + } + advancedChars = true; + break; + } + } + + if (!advancedChars) { + // If both lines are totally revealed, see if we can scroll up a line. + // This is doable as long as the next line isn't the sentinel value that + // means an A press is required. + if (!lines_.empty() && lines_.front() != "\n") { + if (linesToShow_.size() == 2) { + linesToShow_.pop_front(); + } + + linesToShow_.push_back(MessageLine { .text = lines_.front() }); + lines_.pop_front(); + } + } + } + } } } @@ -24,6 +76,107 @@ void MessageSystem::hideCutsceneBars() { barsState_ = BarsState::Closing; } +void MessageSystem::displayMessage(std::string_view msg, SpeakerType speaker) { + if (!(barsState_ == BarsState::Opening || barsState_ == BarsState::Open)) { + displayCutsceneBars(); + } + + speaker_ = speaker; + + auto lineChunks = splitStr>(std::string(msg), "\n"); + for (const std::string& text : lineChunks) { + auto words = splitStr>(text, " "); + + std::string curLine; + int curWidth = 0; + bool firstWord = true; + bool shouldAddBlank = false; + + // I'm gonna be frank and admit it: I'm not gonna take hyphenation into + // consideration. Please don't write any words that are wider than the + // textbox. + for (const std::string& word : words) { + int wordWidth = 0; + bool firstChar = true; + for (int i=0; i MESSAGE_TEXT_WIDTH) { + lines_.push_back(curLine); + curLine = word; + curWidth = wordWidth + game_.getFont().getCharacterWidth(' '); + + if (shouldAddBlank) { + shouldAddBlank = false; + lines_.push_back("\n"); + } else { + shouldAddBlank = true; + } + } else { + curWidth = nextWidth; + if (!firstWord) { + curLine.append(" "); + } + curLine.append(word); + } + + firstWord = false; + } + + lines_.push_back(curLine); + lines_.push_back("\n"); + } + + if (linesToShow_.empty()) { + linesToShow_.push_back(MessageLine { .text = lines_.front() }); + lines_.pop_front(); + + if (lines_.front() != "\n") { + linesToShow_.push_back(MessageLine { .text = lines_.front() }); + lines_.pop_front(); + } + } +} + +void MessageSystem::advanceText() { + if (barsState_ != BarsState::Open) { + return; + } + + for (const MessageLine& line : linesToShow_) { + if (line.charsRevealed != line.text.size()) { + return; + } + } + + if (lines_.empty()) { + linesToShow_.clear(); + return; + } + + if (lines_.front() != "\n") { + return; + } + + lines_.pop_front(); + + if (lines_.empty()) { + linesToShow_.clear(); + } +} + double MessageSystem::getCutsceneBarsProgress() const { switch (barsState_) { case BarsState::Closed: return 0.0; diff --git a/src/message_system.h b/src/message_system.h index 4dd0166..153805a 100644 --- a/src/message_system.h +++ b/src/message_system.h @@ -1,10 +1,23 @@ #ifndef MESSAGE_SYSTEM_H_DE10D011 #define MESSAGE_SYSTEM_H_DE10D011 +#include +#include +#include #include "system.h" +#include "timer.h" class Game; +enum class SpeakerType { + None, + Man, + Woman, + Boy, + Girl, + Nonhuman +}; + class MessageSystem : public System { public: @@ -20,10 +33,21 @@ public: void hideCutsceneBars(); + void displayMessage(std::string_view msg, SpeakerType speaker); + + void advanceText(); + // Info double getCutsceneBarsProgress() const; + struct MessageLine { + std::string text; + int charsRevealed = 0; + }; + + const std::list& getLines() const { return linesToShow_; } + private: enum class BarsState { @@ -37,6 +61,10 @@ private: BarsState barsState_ = BarsState::Closed; double accum_ = 0.0; double length_ = 1000.0/8; + SpeakerType speaker_ = SpeakerType::None; + std::list lines_; + std::list linesToShow_; + Timer textAdvTimer_ { 10 }; }; #endif /* end of include guard: MESSAGE_SYSTEM_H_DE10D011 */ diff --git a/src/renderer.cpp b/src/renderer.cpp index 2c6cf5c..b22c316 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -1,4 +1,5 @@ #include "renderer.h" +#include #include "consts.h" #include "game.h" #include "map.h" @@ -153,6 +154,33 @@ void Renderer::render(Game& game) { int bottomBarHeight = 36.0 * game.getSystem().getCutsceneBarsProgress(); SDL_Rect bottomBar { 0, CANVAS_HEIGHT - bottomBarHeight, CANVAS_WIDTH, bottomBarHeight }; SDL_RenderFillRect(ren_.get(), &bottomBar); + + if (game.getSystem().getCutsceneBarsProgress() == 1.0 && + !game.getSystem().getLines().empty()) { + int lineIndex = 0; + for (const MessageSystem::MessageLine& line : game.getSystem().getLines()) { + if (messageLines_[lineIndex].line != line.text) { + renderMessageLine(lineIndex, line.text, game); + } + + // 18x, 127y1, 143y2 + if (line.charsRevealed > 0) { + SDL_Rect srcRect { + 0, 0, messageLines_[lineIndex].charIndexToWidth[line.charsRevealed], + game.getFont().getCharacterHeight() + }; + SDL_Rect destRect { + 18, + 127 + 16 * lineIndex, + srcRect.w, + srcRect.h }; + + SDL_RenderCopy(ren_.get(), messageLines_[lineIndex].renderedTex.get(), &srcRect, &destRect); + } + + lineIndex++; + } + } } SDL_SetRenderTarget(ren_.get(), nullptr); @@ -175,3 +203,46 @@ int Renderer::loadImageFromFile(std::string_view filename) { return texId; } + +void Renderer::renderMessageLine(int lineIndex, const std::string& text, Game& game) { + MessageCache& line = messageLines_[lineIndex]; + line.line = text; + + line.renderedTex.reset( + SDL_CreateTexture( + ren_.get(), + SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_TARGET, + MESSAGE_TEXT_WIDTH, + game.getFont().getCharacterHeight())); + + SDL_SetTextureBlendMode(line.renderedTex.get(), SDL_BLENDMODE_BLEND); + + SDL_SetRenderTarget(ren_.get(), line.renderedTex.get()); + SDL_SetRenderDrawBlendMode(ren_.get(), SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(ren_.get(), 0, 0, 0, 0); + SDL_RenderClear(ren_.get()); + + line.charIndexToWidth.clear(); + line.charIndexToWidth.push_back(0); + + int length = 0; + + for (int i=0; i #include #include +#include #include class Game; @@ -124,6 +125,8 @@ private: texture_ptr renderMapLayer(const Map& map, int layer); + void renderMessageLine(int lineIndex, const std::string& text, Game& game); + sdl_wrapper sdl_; img_wrapper img_; window_ptr win_; @@ -132,6 +135,14 @@ private: std::vector textures_; texture_ptr renLay0_; texture_ptr renLay1_; + + struct MessageCache { + texture_ptr renderedTex; + std::vector charIndexToWidth; + std::string line; + }; + + MessageCache messageLines_[2]; }; #endif /* end of include guard: RENDERER_H_6A58EC30 */ -- cgit 1.4.1