bool Rect::contains(const Int2 &point) const { return (point.getX() >= this->getLeft()) && (point.getY() >= this->getTop()) && (point.getX() < this->getRight()) && (point.getY() < this->getBottom()); }
Renderer::Renderer(int width, int height, bool fullscreen, double letterboxAspect) { Debug::mention("Renderer", "Initializing."); assert(width > 0); assert(height > 0); this->letterboxAspect = letterboxAspect; // Initialize window. The SDL_Surface is obtained from this window. this->window = [width, height, fullscreen]() { std::string title = "OpenTESArena"; return fullscreen ? SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, SDL_WINDOW_FULLSCREEN_DESKTOP) : SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_RESIZABLE); }(); Debug::check(this->window != nullptr, "Renderer", "SDL_CreateWindow"); // Initialize renderer context. this->renderer = this->createRenderer(); // Use window dimensions, just in case it's fullscreen and the given width and // height are ignored. Int2 windowDimensions = this->getWindowDimensions(); // Initialize native frame buffer. this->nativeTexture = this->createTexture(Renderer::DEFAULT_PIXELFORMAT, SDL_TEXTUREACCESS_TARGET, windowDimensions.getX(), windowDimensions.getY()); Debug::check(this->nativeTexture != nullptr, "Renderer", "Couldn't create native frame buffer, " + std::string(SDL_GetError())); // Initialize 320x200 frame buffer. this->originalTexture = this->createTexture(Renderer::DEFAULT_PIXELFORMAT, SDL_TEXTUREACCESS_TARGET, Renderer::ORIGINAL_WIDTH, Renderer::ORIGINAL_HEIGHT); // Don't initialize the game world buffer until the 3D renderer is initialized. this->clProgram = nullptr; this->gameWorldTexture = nullptr; this->fullGameWindow = false; // Set the original frame buffer to not use transparency by default. this->useTransparencyBlending(false); }
Int2 Renderer::originalPointToNative(const Int2 &originalPoint) const { // From original point to letterbox point. double originalXPercent = static_cast<double>(originalPoint.getX()) / static_cast<double>(Renderer::ORIGINAL_WIDTH); double originalYPercent = static_cast<double>(originalPoint.getY()) / static_cast<double>(Renderer::ORIGINAL_HEIGHT); const auto letterbox = this->getLetterboxDimensions(); Int2 letterboxPoint( static_cast<double>(letterbox.w) * originalXPercent, static_cast<double>(letterbox.h) * originalYPercent); // Then from letterbox point to native point. Int2 nativePoint( letterboxPoint.getX() + letterbox.x, letterboxPoint.getY() + letterbox.y); return nativePoint; }
Int2 Renderer::nativePointToOriginal(const Int2 &nativePoint) const { // From native point to letterbox point. Int2 windowDimensions = this->getWindowDimensions(); const auto letterbox = this->getLetterboxDimensions(); Int2 letterboxPoint( nativePoint.getX() - letterbox.x, nativePoint.getY() - letterbox.y); // Then from letterbox point to original point. double letterboxXPercent = static_cast<double>(letterboxPoint.getX()) / static_cast<double>(letterbox.w); double letterboxYPercent = static_cast<double>(letterboxPoint.getY()) / static_cast<double>(letterbox.h); Int2 originalPoint( static_cast<double>(Renderer::ORIGINAL_WIDTH) * letterboxXPercent, static_cast<double>(Renderer::ORIGINAL_HEIGHT) * letterboxYPercent); return originalPoint; }
SDL_Surface *Renderer::getScreenshot() const { const Int2 dimensions = this->getWindowDimensions(); SDL_Surface *screenshot = Surface::createSurfaceWithFormat( dimensions.getX(), dimensions.getY(), Renderer::DEFAULT_BPP, Renderer::DEFAULT_PIXELFORMAT); int status = SDL_RenderReadPixels(this->renderer, nullptr, screenshot->format->format, screenshot->pixels, screenshot->pitch); if (status == 0) { Debug::mention("Renderer", "Screenshot taken."); } else { Debug::crash("Renderer", "Couldn't take screenshot, " + std::string(SDL_GetError())); } return screenshot; }
GameWorldPanel::GameWorldPanel(GameState *gameState) : Panel(gameState) { assert(gameState->gameDataIsActive()); this->playerNameTextBox = [gameState]() { int x = 17; int y = 154; Color color(215, 121, 8); std::string text = gameState->getGameData()->getPlayer().getFirstName(); auto &font = gameState->getFontManager().getFont(FontName::Char); auto alignment = TextAlignment::Left; return std::unique_ptr<TextBox>(new TextBox( x, y, color, text, font, alignment, gameState->getRenderer())); }(); this->automapButton = []() { auto function = [](GameState *gameState) { std::unique_ptr<Panel> automapPanel(new AutomapPanel(gameState)); gameState->setPanel(std::move(automapPanel)); }; return std::unique_ptr<Button>(new Button(function)); }(); this->characterSheetButton = []() { auto function = [](GameState *gameState) { std::unique_ptr<Panel> sheetPanel(new CharacterPanel(gameState)); gameState->setPanel(std::move(sheetPanel)); }; return std::unique_ptr<Button>(new Button(function)); }(); this->logbookButton = []() { auto function = [](GameState *gameState) { std::unique_ptr<Panel> logbookPanel(new LogbookPanel(gameState)); gameState->setPanel(std::move(logbookPanel)); }; return std::unique_ptr<Button>(new Button(function)); }(); this->pauseButton = []() { auto function = [](GameState *gameState) { std::unique_ptr<Panel> pausePanel(new PauseMenuPanel(gameState)); gameState->setPanel(std::move(pausePanel)); }; return std::unique_ptr<Button>(new Button(function)); }(); this->worldMapButton = []() { auto function = [](GameState *gameState) { std::unique_ptr<Panel> mapPanel(new WorldMapPanel(gameState)); gameState->setPanel(std::move(mapPanel)); }; return std::unique_ptr<Button>(new Button(function)); }(); // Set all of the cursor regions relative to the current window. const Int2 screenDims = gameState->getRenderer().getWindowDimensions(); this->updateCursorRegions(screenDims.getX(), screenDims.getY()); }
void GameWorldPanel::render(Renderer &renderer) { assert(this->getGameState()->gameDataIsActive()); // Clear full screen. renderer.clearNative(); renderer.clearOriginal(); // Draw game world onto the native frame buffer. The game world buffer // might not completely fill up the native buffer (bottom corners), so // clearing the native buffer beforehand is still necessary. renderer.renderWorld(); // Set screen palette. auto &textureManager = this->getGameState()->getTextureManager(); textureManager.setPalette(PaletteFile::fromName(PaletteName::Default)); // Set original frame buffer blending to true. renderer.useTransparencyBlending(true); // Draw game world interface. const auto &gameInterface = textureManager.getTexture( TextureFile::fromName(TextureName::GameWorldInterface)); renderer.drawToOriginal(gameInterface.get(), 0, Renderer::ORIGINAL_HEIGHT - gameInterface.getHeight()); // Draw player portrait. const auto &player = this->getGameState()->getGameData()->getPlayer(); const auto &headsFilename = PortraitFile::getHeads( player.getGenderName(), player.getRaceName(), true); const auto &portrait = textureManager.getTextures(headsFilename) .at(player.getPortraitID()); const auto &status = textureManager.getTextures( TextureFile::fromName(TextureName::StatusGradients)).at(0); renderer.drawToOriginal(status.get(), 14, 166); renderer.drawToOriginal(portrait.get(), 14, 166); // Draw compass slider (the actual headings). +X is north, +Z is east. // Should do some sin() and cos() functions to get the pixel offset. auto *compassSlider = textureManager.getSurface( TextureFile::fromName(TextureName::CompassSlider)); Texture compassSliderSegment = [&renderer, &compassSlider]() { SDL_Surface *segmentTemp = Surface::createSurfaceWithFormat(32, 7, Renderer::DEFAULT_BPP, Renderer::DEFAULT_PIXELFORMAT); SDL_Rect clipRect; clipRect.x = 60; // Arbitrary offset until compass rotation works. clipRect.y = 0; clipRect.w = segmentTemp->w; clipRect.h = segmentTemp->h; SDL_BlitSurface(compassSlider, &clipRect, segmentTemp, nullptr); SDL_Texture *segment = renderer.createTextureFromSurface(segmentTemp); SDL_FreeSurface(segmentTemp); return Texture(segment); }(); renderer.drawToOriginal(compassSliderSegment.get(), (Renderer::ORIGINAL_WIDTH / 2) - (compassSliderSegment.getWidth() / 2), compassSliderSegment.getHeight()); // Draw compass frame over the headings. const auto &compassFrame = textureManager.getTexture( TextureFile::fromName(TextureName::CompassFrame)); renderer.drawToOriginal(compassFrame.get(), (Renderer::ORIGINAL_WIDTH / 2) - (compassFrame.getWidth() / 2), 0); // If the player's class can't use magic, show the darkened spell icon. if (!player.getCharacterClass().canCastMagic()) { const auto &nonMagicIcon = textureManager.getTexture( TextureFile::fromName(TextureName::NoSpell)); renderer.drawToOriginal(nonMagicIcon.get(), 91, 177); } // Draw text: player name. renderer.drawToOriginal(this->playerNameTextBox->getTexture(), this->playerNameTextBox->getX(), this->playerNameTextBox->getY()); // Scale the original frame buffer onto the native one. // This shouldn't be done for the game world interface because it needs to // clamp to the screen edges, not the letterbox edges. // Fix this eventually... again. renderer.drawOriginalToNative(); // Draw cursor, depending on its position on the screen. const Int2 mousePosition = this->getMousePosition(); const Texture &cursor = [this, &mousePosition, &textureManager]() -> const Texture& // Interesting how this return type isn't deduced in MSVC. { // See which arrow cursor region the native mouse is in. for (int i = 0; i < this->nativeCursorRegions.size(); ++i) { if (this->nativeCursorRegions.at(i)->contains(mousePosition)) { return textureManager.getTextures( TextureFile::fromName(TextureName::ArrowCursors)).at(i); } } // If not in any of the arrow regions, use the default sword cursor. return textureManager.getTexture( TextureFile::fromName(TextureName::SwordCursor)); }(); renderer.drawToNative(cursor.get(), mousePosition.getX(), mousePosition.getY(), static_cast<int>(cursor.getWidth() * this->getCursorScale()), static_cast<int>(cursor.getHeight() * this->getCursorScale())); // Set the transparency blending back to normal (off). renderer.useTransparencyBlending(false); }
void GameWorldPanel::handleMouse(double dt) { static_cast<void>(dt); const auto &renderer = this->getGameState()->getRenderer(); const uint32_t mouse = SDL_GetRelativeMouseState(nullptr, nullptr); const bool leftClick = (mouse & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0; if (leftClick) { // Horizontal camera movement rough draft. The original camera controls for // Arena are bad, but I am simulating them before thinking of adding modern // 3D camera support (like Daggerfall) as an option. const Int2 screenDimensions = renderer.getWindowDimensions(); const Int2 mousePosition = this->getMousePosition(); // Strength of turning is determined by proximity of the mouse cursor to // the left or right screen edge. const double dx = [this, &mousePosition, &screenDimensions]() { const int mouseX = mousePosition.getX(); // Native cursor regions (scaled to the current window). const Rect &middleLeft = *this->nativeCursorRegions.at(3).get(); const Rect &middleRight = *this->nativeCursorRegions.at(5).get(); // Measure the magnitude of rotation. -1.0 is left, 1.0 is right. double percent = 0.0; if (middleLeft.contains(mousePosition)) { percent = -1.0 + (static_cast<double>(mouseX) / middleLeft.getWidth()); } else if (middleRight.contains(mousePosition)) { percent = static_cast<double>(mouseX - middleRight.getLeft()) / middleRight.getWidth(); } // Reduce the magnitude by a lot as a baseline. Sensitivity can be // tweaked in the options. percent *= 0.010; // No NaNs or infinities allowed. return std::isfinite(percent) ? percent : 0.0; }(); auto &player = this->getGameState()->getGameData()->getPlayer(); const auto &options = this->getGameState()->getOptions(); // Yaw the camera left or right. No vertical movement in classic camera mode. player.rotate(dx, 0.0, options.getHorizontalSensitivity(), options.getVerticalSensitivity(), options.getVerticalFOV()); } // Later in development, a 3D camera would be fun (more like Daggerfall), but // for now the objective is to more closely resemble the original game, so the // rough draft 3D camera code below is commented out as a result. // Make the camera look around. /*int dx, dy; const auto mouse = SDL_GetRelativeMouseState(&dx, &dy); bool leftClick = (mouse & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0; bool rightClick = (mouse & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0; bool turning = ((dx != 0) || (dy != 0)) && leftClick; if (turning) { auto dimensions = this->getGameState()->getRenderer().getWindowDimensions(); double dxx = static_cast<double>(dx) / static_cast<double>(dimensions.getX()); double dyy = static_cast<double>(dy) / static_cast<double>(dimensions.getY()); // Pitch and/or yaw the camera. const auto &options = this->getGameState()->getOptions(); this->getGameState()->getGameData()->getPlayer().rotate(dxx, -dyy, options.getHorizontalSensitivity(), options.getVerticalSensitivity(), options.getVerticalFOV()); }*/ }