From 30e87c47b7cbf793e269fe4a44ca164486f10312 Mon Sep 17 00:00:00 2001 From: Hume2 Date: Sat, 25 May 2024 13:19:56 +0200 Subject: [PATCH] Scale paralax layers perspectively-correctly --- src/badguy/dispenser.cpp.orig | 439 ++++++++++++++++++++++++++++++++++ src/object/background.cpp | 11 +- src/object/tilemap.cpp | 15 +- src/video/drawing_context.cpp | 23 ++ src/video/drawing_context.hpp | 3 + 5 files changed, 485 insertions(+), 6 deletions(-) create mode 100644 src/badguy/dispenser.cpp.orig diff --git a/src/badguy/dispenser.cpp.orig b/src/badguy/dispenser.cpp.orig new file mode 100644 index 00000000000..ac1d4385820 --- /dev/null +++ b/src/badguy/dispenser.cpp.orig @@ -0,0 +1,439 @@ +// SuperTux +// Copyright (C) 2006 Matthias Braun +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "badguy/dispenser.hpp" + +#include "audio/sound_manager.hpp" +#include "editor/editor.hpp" +#include "math/random.hpp" +#include "object/bullet.hpp" +#include "object/player.hpp" +#include "sprite/sprite.hpp" +#include "supertux/flip_level_transformer.hpp" +#include "supertux/game_object_factory.hpp" +#include "supertux/sector.hpp" +#include "util/reader_iterator.hpp" +#include "util/reader_mapping.hpp" + +Dispenser::Dispenser(const ReaderMapping& reader) : + BadGuy(reader, "images/creatures/dispenser/dropper.sprite", LAYER_OBJECTS + 5), + ExposedObject(this), + m_cycle(), + m_objects(), + m_next_object(0), + m_dispense_timer(), + m_autotarget(false), + m_random(), + m_gravity(), + m_limit_dispensed_badguys(), + m_max_concurrent_badguys(), + m_current_badguys() +{ + parse_type(reader); + + set_colgroup_active(COLGROUP_MOVING_STATIC); + SoundManager::current()->preload("sounds/squish.wav"); + reader.get("cycle", m_cycle, 5.0f); + if (reader.get("gravity", m_gravity)) m_physic.enable_gravity(true); + reader.get("random", m_random, false); + + std::vector badguys; + if (reader.get("badguy", badguys)) // Backward compatibility. + { + for (auto& badguy : badguys) + add_object(GameObjectFactory::instance().create(badguy)); + } + else + { + std::optional objects_mapping; + if (reader.get("objects", objects_mapping)) + { + auto iter = objects_mapping->get_iter(); + while (iter.next()) + add_object(GameObjectFactory::instance().create(iter.get_key(), iter.as_mapping())); + } + } + + m_dir = m_start_dir; // Reset direction to default. + + reader.get("limit-dispensed-badguys", m_limit_dispensed_badguys, false); + reader.get("max-concurrent-badguys", m_max_concurrent_badguys, 0); + +// if (badguys.size() <= 0) +// throw std::runtime_error("No badguys in dispenser."); + + update_hitbox(); + m_countMe = false; +} + +void +Dispenser::add_object(std::unique_ptr object) +{ + auto moving_object = dynamic_cast(object.get()); + if (!GameObjectFactory::instance().has_params(object->get_class_name(), ObjectFactory::RegisteredObjectParam::OBJ_PARAM_DISPENSABLE) || + !moving_object) // Object is not MovingObject, or is not dispensable. + { + log_warning << object->get_class_name() << " is not dispensable. Removing from dispenser object list." << std::endl; + return; + } + + moving_object->set_parent_dispenser(this); + m_objects.push_back(std::move(object)); +} + +void +Dispenser::draw(DrawingContext& context) +{ + if (m_type != DispenserType::POINT || Editor::is_active()) + BadGuy::draw(context); +} + +void +Dispenser::initialize() +{ + m_dir = m_start_dir; // Reset direction to default. +} + +void +Dispenser::activate() +{ + m_dispense_timer.start(m_cycle, true); + launch_object(); +} + +void +Dispenser::deactivate() +{ + m_dispense_timer.stop(); +} + +HitResponse +Dispenser::collision(GameObject& other, const CollisionHit& hit) +{ + auto bullet = dynamic_cast (&other); + if (bullet) + return collision_bullet(*bullet, hit); + + return FORCE_MOVE; +} + +void +Dispenser::active_update(float dt_sec) +{ + if (m_gravity) + { + BadGuy::active_update(dt_sec); + } + if (m_dispense_timer.check()) + { + auto player = get_nearest_player(); + if (player) + { + if(player->is_dying() || player->is_dead()) + { + return; + } + + // Auto always shoots in Tux's direction. + if (m_autotarget) + { + Direction target_dir = (player->get_pos().x > get_pos().x) ? Direction::RIGHT : Direction::LEFT; + if (m_dir != target_dir) + { + m_dir = target_dir; + return; + } + } + } + + launch_object(); + } +} + +void +Dispenser::launch_object() +{ + if (m_objects.empty()) return; + if (m_frozen) return; + + // FIXME: Does is_offscreen() work right here? + if (!is_offscreen() && !Editor::is_active()) + { + Direction launch_dir = m_dir; + if (!m_autotarget && m_start_dir == Direction::AUTO) + { + Player* player = get_nearest_player(); + if (player) + launch_dir = (player->get_pos().x > get_pos().x) ? Direction::RIGHT : Direction::LEFT; + } + + if (m_objects.size() > 1) + { + if (m_random) + { + m_next_object = static_cast(gameRandom.rand(static_cast(m_objects.size()))); + } + else + { + m_next_object++; + + if (m_next_object >= m_objects.size()) + m_next_object = 0; + } + } + + auto object = m_objects[m_next_object].get(); + auto obj_badguy = dynamic_cast(object); + if (obj_badguy && m_limit_dispensed_badguys && + m_current_badguys >= m_max_concurrent_badguys) + { + // The object is a badguy and max concurrent badguys limit has been reached. + // Do not dispense. + return; + } + + try + { + auto game_object = GameObjectFactory::instance().create(object->get_class_name(), get_pos(), launch_dir, object->save()); + if (!game_object) + { + throw std::runtime_error("Creating " + object->get_class_name() + " object failed."); + } + auto moving_object = static_cast(game_object.get()); + + Rectf object_bbox = moving_object->get_bbox(); + Vector spawnpoint(0.0f, 0.0f); + + switch (m_type) + { + case DispenserType::GRANITO: + case DispenserType::DROPPER: + if (m_flip == NO_FLIP) + { + spawnpoint = get_anchor_pos (m_col.m_bbox, ANCHOR_BOTTOM); + spawnpoint.x -= 0.5f * object_bbox.get_width(); + } + else + { + spawnpoint = get_anchor_pos (m_col.m_bbox, ANCHOR_TOP); + spawnpoint.y -= m_col.m_bbox.get_height(); + spawnpoint.x -= 0.5f * object_bbox.get_width(); + } + break; + + case DispenserType::CANNON: + spawnpoint = get_pos(); /* Top-left corner of the cannon. */ + if (launch_dir == Direction::LEFT) + spawnpoint.x -= object_bbox.get_width() + 1; + else + spawnpoint.x += m_col.m_bbox.get_width() + 1; + if (m_flip != NO_FLIP) + spawnpoint.y += (m_col.m_bbox.get_height() - 20); + break; + + case DispenserType::POINT: + spawnpoint = m_col.m_bbox.p1(); + break; + + default: + break; + } + + /* Now we set the real spawn position. */ + moving_object->set_pos(spawnpoint); + + /* Set reference to dispenser in the object itself. */ + moving_object->set_parent_dispenser(this); + + if (obj_badguy) // The object is a badguy. + { + auto badguy = static_cast(moving_object); + + /* We don't want to count dispensed badguys in level stats. */ + badguy->m_countMe = false; + + if (m_limit_dispensed_badguys) + m_current_badguys++; + } + + Sector::get().add_object(std::move(game_object)); + } + catch(const std::exception& e) + { + log_warning << "Error dispensing object: " << e.what() << std::endl; + return; + } + } +} + +void +Dispenser::freeze() +{ + if (m_type == DispenserType::POINT || m_type == DispenserType::GRANITO) + return; + + set_group(COLGROUP_MOVING_STATIC); + SoundManager::current()->play("sounds/sizzle.ogg", get_pos()); + m_frozen = true; + + const std::string cannon_iced = "iced-" + dir_to_string(m_dir); + if (m_type == DispenserType::CANNON && m_sprite->has_action(cannon_iced)) + { + set_action(cannon_iced, 1); + // When the dispenser is a cannon, it uses the respective "iced" action, based on the current direction. + } + else + { + if (m_type == DispenserType::DROPPER && m_sprite->has_action("dropper-iced")) + { + set_action("dropper-iced", 1); + // When the dispenser is a dropper, it uses the "dropper-iced". + } + else + { + m_sprite->set_color(Color(0.6f, 0.72f, 0.88f)); + m_sprite->stop_animation(); + // When the dispenser is something else (unprobable), or has no matching iced sprite, it shades to blue. + } + } + m_dispense_timer.stop(); +} + +void +Dispenser::unfreeze(bool melt) +{ + /*set_group(colgroup_active); + frozen = false; + + sprite->set_color(Color(1.00, 1.00, 1.00f));*/ + BadGuy::unfreeze(melt); + + set_colgroup_active(m_type == DispenserType::POINT ? COLGROUP_DISABLED : + COLGROUP_MOVING_STATIC); + set_correct_action(); + activate(); +} + +bool +Dispenser::is_freezable() const +{ + return true; +} + +bool +Dispenser::is_flammable() const +{ + return false; +} + +bool +Dispenser::is_portable() const +{ + return false; +} + +void +Dispenser::set_correct_action() +{ + switch (m_type) + { + case DispenserType::CANNON: + set_action(m_dir); + break; + case DispenserType::POINT: + set_colgroup_active(COLGROUP_DISABLED); + break; + default: + break; + } +} + +void +Dispenser::on_type_change(int old_type) +{ + MovingSprite::on_type_change(old_type); +<<<<<<< HEAD + + if (old_type == GRANITO || m_type == GRANITO) + { + m_objects.clear(); + if (m_type == GRANITO) // Switching to type GRANITO + add_object(GameObjectFactory::instance().create("corrupted_granito")); + } + +======= +>>>>>>> Check if `MovingSprite` type change call is initial + set_correct_action(); +} + +ObjectSettings +Dispenser::get_settings() +{ + ObjectSettings result = BadGuy::get_settings(); + + result.add_float(_("Interval (seconds)"), &m_cycle, "cycle"); + result.add_bool(_("Random"), &m_random, "random", false); + if (m_type != GRANITO) + { + result.add_objects(_("Objects"), &m_objects, ObjectFactory::RegisteredObjectParam::OBJ_PARAM_DISPENSABLE, + [this](auto obj) { add_object(std::move(obj)); }, "objects"); + } + result.add_bool(_("Limit dispensed badguys"), &m_limit_dispensed_badguys, + "limit-dispensed-badguys", false); + result.add_bool(_("Obey Gravity"), &m_gravity, + "gravity", false); + result.add_int(_("Max concurrent badguys"), &m_max_concurrent_badguys, + "max-concurrent-badguys", 0); + + result.reorder({"cycle", "random", "type", "objects", "direction", "gravity", "limit-dispensed-badguys", "max-concurrent-badguys", "x", "y"}); + + return result; +} + +GameObjectTypes +Dispenser::get_types() const +{ + return { + { "dropper", _("Dropper") }, + { "cannon", _("Cannon") }, + { "point", _("Invisible") }, + { "granito", _("Granito") } + }; +} + +std::string +Dispenser::get_default_sprite_name() const +{ + switch (m_type) + { + case POINT: + return "images/creatures/dispenser/invisible.sprite"; + case GRANITO: + return "images/creatures/granito/corrupted/hive/granito_hive.sprite"; + default: + return "images/creatures/dispenser/" + type_value_to_id(m_type) + ".sprite"; + } +} + +void +Dispenser::on_flip(float height) +{ + BadGuy::on_flip(height); + if (!m_gravity) + FlipLevelTransformer::transform_flip(m_flip); +} + +/* EOF */ diff --git a/src/object/background.cpp b/src/object/background.cpp index 66ba9657ac1..bef95cf79b4 100644 --- a/src/object/background.cpp +++ b/src/object/background.cpp @@ -297,7 +297,6 @@ Background::draw_image(DrawingContext& context, const Vector& pos_) const int end_y = static_cast(ceilf((cliprect.get_bottom() - (pos_.y + img_h/2.0f)) / img_h)) + 1; Canvas& canvas = context.get_canvas(m_target); - context.set_flip(context.get_flip() ^ m_flip); if (m_fill) { @@ -370,7 +369,6 @@ Background::draw_image(DrawingContext& context, const Vector& pos_) break; } } - context.set_flip(context.get_flip() ^ m_flip); } void @@ -381,6 +379,14 @@ Background::draw(DrawingContext& context) if (!m_image) return; + + context.push_transform(); + if (!context.perspective_scale(m_parallax_speed.x, m_parallax_speed.y)) { + //The background is placed behind the camera. + context.pop_transform(); + return; + } + context.set_flip(context.get_flip() ^ m_flip); Sizef level_size(d_gameobject_manager->get_width(), d_gameobject_manager->get_height()); @@ -394,6 +400,7 @@ Background::draw(DrawingContext& context) level_size.height / 2); draw_image(context, pos + m_scroll_offset + Vector(center_offset.x * (1.0f - m_parallax_speed.x), center_offset.y * (1.0f - m_parallax_speed.y))); + context.pop_transform(); } namespace { diff --git a/src/object/tilemap.cpp b/src/object/tilemap.cpp index ab0d23f87bb..a7ae84240fd 100644 --- a/src/object/tilemap.cpp +++ b/src/object/tilemap.cpp @@ -438,9 +438,18 @@ TileMap::draw(DrawingContext& context) { // skip draw if current opacity is 0.0 if (m_current_alpha == 0.0f) return; - + context.push_transform(); + const bool normal_speed = m_editor_active && Editor::is_active(); + float speed_x = normal_speed ? 1.0f : m_speed_x; + float speed_y = normal_speed ? 1.0f : m_speed_y; + if (!context.perspective_scale(speed_x, speed_y)) { + //The tilemap is placed behind the camera. + context.pop_transform(); + return; + } + if (m_flip != NO_FLIP) context.set_flip(m_flip); if (m_editor_active) { @@ -453,9 +462,7 @@ TileMap::draw(DrawingContext& context) const float trans_x = context.get_translation().x; const float trans_y = context.get_translation().y; - const bool normal_speed = m_editor_active && Editor::is_active(); - context.set_translation(Vector(trans_x * (normal_speed ? 1.0f : m_speed_x), - trans_y * (normal_speed ? 1.0f : m_speed_y))); + context.set_translation(Vector(trans_x*speed_x, trans_y*speed_y)); Rectf draw_rect = context.get_cliprect(); Rect t_draw_rect = get_tiles_overlapping(draw_rect); diff --git a/src/video/drawing_context.cpp b/src/video/drawing_context.cpp index 6a753f2761c..e3a4cdb5193 100644 --- a/src/video/drawing_context.cpp +++ b/src/video/drawing_context.cpp @@ -132,4 +132,27 @@ DrawingContext::get_size() const return Vector(get_width(), get_height()) * transform().scale; } +bool +DrawingContext::perspective_scale(float speed_x, float speed_y) +{ + DrawingTransform& tfm = transform(); + if (tfm.scale == 1 || speed_x < 0 || speed_y < 0) { + //Trivial or unreal situation: Do not apply perspective. + return true; + } + const float speed = sqrt(speed_x*speed_y); + if (speed == 0) { + //Special case: The object appears to be infinitely far. + tfm.scale = 1.0; + return true; + } + const float t = tfm.scale*(1/speed - 1) + 1; + if (t <= 0) { + //The object will appear behind the camera, therefore we shall not see it. + return false; + } + tfm.scale /= speed * t; + return true; +} + /* EOF */ diff --git a/src/video/drawing_context.hpp b/src/video/drawing_context.hpp index 53a394afaa2..2b33134ba30 100644 --- a/src/video/drawing_context.hpp +++ b/src/video/drawing_context.hpp @@ -77,6 +77,9 @@ class DrawingContext final float get_scale() const { return transform().scale; } void scale(float scale) { transform().scale *= scale; } + + /** Recalculates the scaling factor for paralax layers.*/ + bool perspective_scale(float speed_x, float speed_y); /** Apply that flip in the next draws (flips are listed on surface.h). */ void set_flip(Flip flip);