diff --git a/assets/scenes/Game.scene b/assets/scenes/Game.scene index 87c5bdf..a90b4d5 100644 --- a/assets/scenes/Game.scene +++ b/assets/scenes/Game.scene @@ -26,6 +26,9 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GameState.h b/src/GameState.h index 4450149..7f9f091 100644 --- a/src/GameState.h +++ b/src/GameState.h @@ -8,11 +8,13 @@ #ifndef MODEL_GAMESTATE_H_ #define MODEL_GAMESTATE_H_ +#include +#include #include #include #include -#include #include +#include #include #include @@ -42,6 +44,8 @@ namespace farmlands { std::vector seedsPrefabs; std::vector plantsPrefabs; + utils::QTree* colliderTree; + // Timing float time; uint32_t date; diff --git a/src/components/basic/Collider.cpp b/src/components/basic/Collider.cpp new file mode 100644 index 0000000..8dfea75 --- /dev/null +++ b/src/components/basic/Collider.cpp @@ -0,0 +1,187 @@ +/* + * Collider.cpp + * + * Created on: Dec 15, 2016 + * Author: tibi + */ + +#include +#include +#include +#include + +using namespace farmlands::utils; + +namespace farmlands { +namespace components { +namespace basic { + +static const float CollisionCheckDistance = 4.0f; + + +Collider::Collider() + : collisionRect(0, 0, 1, 1), + solid(false), + m_sprite(nullptr), + m_lastBounds(0, 0, 0, 0), + m_lastX(0), m_lastY(0) +{ +} + +Collider::~Collider() +{ + auto qtree = GameState::current().colliderTree; + if (!qtree) + return; + + // Remove self from the qtree + auto it0 = qtree->find(this, m_lastBounds.x, m_lastBounds.y); + + // Object may have never been initialized, so it is possible that it is not in the qtree + if (it0 != qtree->end()) + { + qtree->erase(it0); + + if (m_sprite) + { + auto it1 = qtree->find(this, m_lastBounds.x + m_lastBounds.w, m_lastBounds.y); + qtree->erase(it1); + auto it2 = qtree->find(this, m_lastBounds.x, m_lastBounds.y + m_lastBounds.h); + qtree->erase(it2); + auto it3 = qtree->find(this, m_lastBounds.x + m_lastBounds.w, m_lastBounds.y + m_lastBounds.h); + qtree->erase(it3); + } + } +} + +model::Component* Collider::clone() +{ + Collider* clone = new Collider(); + clone->collisionRect = collisionRect; + clone->solid = solid; + return clone; +} + +void Collider::dump(unsigned level) +{ + for (unsigned i = 0; i < level; i++) + std::cout<<" "; + + std::cout << " .Component: Collider\n"; +} + +void Collider::onInitialize() +{ + auto qtree = GameState::current().colliderTree; + Assert(qtree != nullptr, "Collider tree not set!"); + + // Obtain sprite + m_sprite = gameObject->component(); + recomputeBounds(); + + qtree->insert(this, m_lastBounds.x, m_lastBounds.y); + if (m_sprite) + { + qtree->insert(this, m_lastBounds.x + m_lastBounds.w, m_lastBounds.y); + qtree->insert(this, m_lastBounds.x, m_lastBounds.y + m_lastBounds.h); + qtree->insert(this, m_lastBounds.x + m_lastBounds.w, m_lastBounds.y + m_lastBounds.h); + } +} + +void Collider::onUpdateLogic() +{ + auto qtree = GameState::current().colliderTree; + Assert(qtree != nullptr, "Collider tree not set!"); + + // Update position if object moved. + // As an optimization, we ignore sprite shape changes (i.e. scale) because + // we expect that to be used for animation. + bool moved = (m_lastX != gameObject->transform.x || m_lastY != gameObject->transform.y); + + if (moved) + { + RectF oldBounds = m_lastBounds; + recomputeBounds(); + + auto it0 = qtree->find(this, oldBounds.x, oldBounds.y); + qtree->move(it0, m_lastBounds.x, m_lastBounds.y); + + if (m_sprite) + { + auto it1 = qtree->find(this, oldBounds.x + oldBounds.w, oldBounds.y); + auto it2 = qtree->find(this, oldBounds.x, oldBounds.y + oldBounds.h); + auto it3 = qtree->find(this, oldBounds.x + oldBounds.w, oldBounds.y + oldBounds.h); + + qtree->move(it1, m_lastBounds.x + m_lastBounds.w, m_lastBounds.y); + qtree->move(it2, m_lastBounds.x, m_lastBounds.y + m_lastBounds.h); + qtree->move(it3, m_lastBounds.x + m_lastBounds.w, m_lastBounds.y + m_lastBounds.h); + } + } +} + +Collider* Collider::checkCollision() +{ + auto qtree = GameState::current().colliderTree; + Assert(qtree != nullptr, "Collider tree not set!"); + + RectF searchArea(m_lastX - CollisionCheckDistance, m_lastY - CollisionCheckDistance, 2 * CollisionCheckDistance, 2 * CollisionCheckDistance); + + for (auto it = qtree->lower_bound(searchArea); it != qtree->upper_bound(searchArea); it++) + { + if (it->data != this && m_lastBounds.intersects(it->data->m_lastBounds)) + return it->data; + } + + return nullptr; +} + +bool Collider::canMove(float newX, float newY) +{ + auto qtree = GameState::current().colliderTree; + Assert(qtree != nullptr, "Collider tree not set!"); + + // Both objects must be solid to register a collision + if (!solid) + return true; + + // Compute moved bounds + float offX = m_lastBounds.x - m_lastX; + float offY = m_lastBounds.y - m_lastY; + RectF movedBounds(newX + offX, newY + offY, m_lastBounds.w, m_lastBounds.h); + + // Search for objects + RectF searchArea(newX - CollisionCheckDistance, newY - CollisionCheckDistance, 2 * CollisionCheckDistance, 2 * CollisionCheckDistance); + for (auto it = qtree->lower_bound(searchArea); it != qtree->upper_bound(searchArea); it++) + { + if (it->data != this && it->data->solid && movedBounds.intersects(it->data->m_lastBounds)) + return false; + } + + return true; +} + +void Collider::recomputeBounds() +{ + m_lastX = gameObject->transform.x; + m_lastY = gameObject->transform.y; + + if (m_sprite) + { + RectF spriteBounds = m_sprite->boundaries(); + m_lastBounds.x = spriteBounds.x + spriteBounds.w * collisionRect.x; + m_lastBounds.y = spriteBounds.y + spriteBounds.h * collisionRect.y; + m_lastBounds.w = spriteBounds.w * collisionRect.w; + m_lastBounds.h = spriteBounds.h * collisionRect.h; + } + else + { + m_lastBounds.x = m_lastX; + m_lastBounds.y = m_lastY; + m_lastBounds.w = 0; + m_lastBounds.h = 0; + } +} + +} /* namespace basic */ +} /* namespace components */ +} /* namespace farmlands */ diff --git a/src/components/basic/Collider.h b/src/components/basic/Collider.h new file mode 100644 index 0000000..38151b4 --- /dev/null +++ b/src/components/basic/Collider.h @@ -0,0 +1,65 @@ +/* + * Collider.h + * + * Created on: Dec 15, 2016 + * Author: tibi + */ + +#ifndef COMPONENTS_BASIC_COLLIDER_H_ +#define COMPONENTS_BASIC_COLLIDER_H_ + +#include +#include +#include + +namespace farmlands { +namespace components { +namespace basic { + + class Collider: public model::Component + { + public: + Collider(); + virtual ~Collider(); + + virtual model::Component* clone() override; + virtual void dump(unsigned level) override; + virtual void onInitialize() override; + virtual void onUpdateLogic() override; + + /** + * Computes the collision bounds. + */ + utils::RectF collisionBoundaries() const { return m_lastBounds; } + + /** + * Checks if this object collides with another Collider object. + */ + Collider* checkCollision(); + + /** + * Tests if this object can move to the given coordinates. + * Both objects must be solid to register a collision. + */ + bool canMove(float newX, float newY); + + // Collision rectangle, relative to sprite bounds (default is [0, 0, 1, 1]) + // If object doesn't have a sprite, this property is not used + utils::RectF collisionRect; + + // If an object is solid, it cannot be passed through. + bool solid; + + private: + void recomputeBounds(); + + Sprite* m_sprite; + utils::RectF m_lastBounds; + float m_lastX, m_lastY; + }; + +} /* namespace basic */ +} /* namespace components */ +} /* namespace farmlands */ + +#endif /* COMPONENTS_BASIC_COLLIDER_H_ */ diff --git a/src/components/npc/MovingNpcController.cpp b/src/components/npc/MovingNpcController.cpp new file mode 100644 index 0000000..b8ab098 --- /dev/null +++ b/src/components/npc/MovingNpcController.cpp @@ -0,0 +1,163 @@ +/* + * MovingNpcController.cpp + * + * Created on: Dec 17, 2016 + * Author: tibi + */ + +#include +#include +#include +#include +#include + +#include + +using namespace farmlands::components::basic; +using namespace farmlands::components::items; +using namespace farmlands::graphics; +using namespace farmlands::input; +using namespace farmlands::model; + +namespace farmlands { +namespace components { +namespace npc { + +static const float NpcWalkVelocity = 3.0f; // The default velocity of the player when walking (units/sec). + +MovingNpcController::MovingNpcController() + : facingDirection(Direction::South), + walking(false), + m_collider(nullptr), + m_currentDestination(0), + m_waitTime(0) +{ +} + +MovingNpcController::~MovingNpcController() +{ +} + +Component* MovingNpcController::clone() +{ + MovingNpcController* clone = new MovingNpcController(); + + // Movement + clone->facingDirection = facingDirection; + clone->walking = walking; + + return clone; +} + +void MovingNpcController::dump(unsigned level) +{ + for (unsigned i = 0; i < level; i++) + std::cout<<" "; + + std::cout << " .Component: MovingNpcController\n"; +} + +void MovingNpcController::onInitialize() +{ + m_collider = gameObject->component(); + + static const int offX[] = { 0, 1, 1, 0 }; + static const int offY[] = { 0, 0, 1, 1 }; + + for (size_t i = 0; i < 4; i++) + { + m_destinationsX[i] = gameObject->transform.x + offX[i] * 10; + m_destinationsY[i] = gameObject->transform.y + offY[i] * 10; + } +} + +void MovingNpcController::onUpdateLogic() +{ + updateMovement(); +} + +void MovingNpcController::onPreRender() +{ + preRenderMovement(); +} + +void MovingNpcController::preRenderMovement() +{ + // Get sprite + Sprite* sprite = gameObject->component(); + + // Compute current state + std::string stateName = (walking) ? "Walking " : "Idle "; + + switch (facingDirection) + { + case Direction::East: + stateName += "right"; + break; + case Direction::West: + stateName += "left"; + break; + case Direction::North: + stateName += "up"; + break; + case Direction::South: + stateName += "down"; + break; + } + + sprite->setState(stateName); +} + +void MovingNpcController::updateMovement() +{ + walking = false; + if (m_waitTime > 0) + { + m_waitTime -= GameState::current().deltaTime; + return; + } + + // Arrived at the destination? + bool arrivedX = std::abs(gameObject->transform.x - m_destinationsX[m_currentDestination]) < 0.5f; + bool arrivedY = std::abs(gameObject->transform.y - m_destinationsY[m_currentDestination]) < 0.5f; + + if (arrivedX && arrivedY) + { + m_currentDestination = (m_currentDestination + 1) % 4; + m_waitTime += 2.0f; + return; + } + + // Simply move + float speed = NpcWalkVelocity * GameState::current().deltaTime; + + float newX = gameObject->transform.x; + float newY = gameObject->transform.y; + moveTowards(&newX, &newY, m_destinationsX[m_currentDestination], m_destinationsY[m_currentDestination], speed); + + if (canMove(newX, newY)) + { + facingDirection = getDirection(newX - gameObject->transform.x, newY - gameObject->transform.y); + gameObject->transform.x = newX; + gameObject->transform.y = newY; + walking = true; + } +} + +Direction MovingNpcController::getDirection(float vx, float vy) +{ + if (fabsf(vx) >= fabsf(vy)) + return (vx > 0) ? Direction::East : Direction::West; + + else + return (vy > 0) ? Direction::South : Direction::North; +} + +bool MovingNpcController::canMove(float x, float y) +{ + return m_collider->canMove(x, y); +} + +} /* namespace npc */ +} /* namespace components */ +} /* namespace farmlands */ diff --git a/src/components/npc/MovingNpcController.h b/src/components/npc/MovingNpcController.h new file mode 100644 index 0000000..3c7940c --- /dev/null +++ b/src/components/npc/MovingNpcController.h @@ -0,0 +1,55 @@ +/* + * MovingNpcController.h + * + * Created on: Dec 17, 2016 + * Author: tibi + */ + +#ifndef COMPONENTS_NPC_MOVINGNPCCONTROLLER_H_ +#define COMPONENTS_NPC_MOVINGNPCCONTROLLER_H_ + +#include +#include + +namespace farmlands { +namespace components { +namespace npc { + + class MovingNpcController: public model::Component + { + public: + MovingNpcController(); + virtual ~MovingNpcController(); + + virtual model::Component* clone() override; + virtual void dump(unsigned level) override; + + virtual void onInitialize() override; + virtual void onUpdateLogic() override; + virtual void onPreRender() override; + + // Movement + model::Direction facingDirection; + bool walking; + + private: + + // Movement + void preRenderMovement(); + void updateMovement(); + bool canMove(float x, float y); + static model::Direction getDirection(float vx, float vy); + + float m_destinationsX[4]; + float m_destinationsY[4]; + size_t m_currentDestination; + float m_waitTime; + + basic::Collider* m_collider; + }; + +} /* namespace npc */ +} /* namespace components */ +} /* namespace farmlands */ + +#endif /* COMPONENTS_NPC_MOVINGNPCCONTROLLER_H_ */ diff --git a/src/components/player/Player.cpp b/src/components/player/Player.cpp index f3fd436..57efb41 100644 --- a/src/components/player/Player.cpp +++ b/src/components/player/Player.cpp @@ -45,6 +45,7 @@ Player::Player() hp(100), maxHp(100), energy(100), maxEnergy(100), money(0), + m_collider(nullptr), m_inventory(nullptr), m_grid(nullptr), m_pickables(nullptr) @@ -92,6 +93,7 @@ void Player::dump(unsigned level) void Player::onInitialize() { + m_collider = gameObject->component(); m_inventory = gameObject->component(); auto root = &GameState::current().scene->root; @@ -310,7 +312,7 @@ void Player::updateMovement() Direction Player::getDirection(float vx, float vy) { - if (vx != 0) + if (fabsf(vx) >= fabsf(vy)) return (vx > 0) ? Direction::East : Direction::West; return (vy > 0) ? Direction::South : Direction::North; } @@ -356,6 +358,11 @@ void Player::performAction(model::GameObject* obj) } } +bool Player::canMove(float x, float y) +{ + return m_collider->canMove(x, y); +} + void Player::updatePickables() { // Don't do anything if inventory is full diff --git a/src/components/player/Player.h b/src/components/player/Player.h index d2538aa..05d1833 100644 --- a/src/components/player/Player.h +++ b/src/components/player/Player.h @@ -8,6 +8,7 @@ #ifndef COMPONENTS_PLAYER_PLAYER_H_ #define COMPONENTS_PLAYER_PLAYER_H_ +#include #include #include #include @@ -69,7 +70,7 @@ namespace player { // Movement void preRenderMovement(); void updateMovement(); - bool canMove(float x, float y) { return true; } + bool canMove(float x, float y); static model::Direction getDirection(float vx, float vy); // Actions @@ -79,6 +80,8 @@ namespace player { // Picking up void updatePickables(); + basic::Collider* m_collider; + basic::Inventory* m_inventory; basic::Grid* m_grid; diff --git a/src/graphics/SpriteRenderer.cpp b/src/graphics/SpriteRenderer.cpp index af5a9fe..9143937 100644 --- a/src/graphics/SpriteRenderer.cpp +++ b/src/graphics/SpriteRenderer.cpp @@ -72,7 +72,7 @@ void SpriteRenderer::onRender() src.h = spriteH; // Draw - SdlRenderer::instance().renderTexture(texture, &src, &dest); + SdlRenderer::instance().queueRenderTexture(texture, &src, &dest, 0); } } diff --git a/src/math/GameMath.cpp b/src/math/GameMath.cpp index 122a138..8b83cbd 100644 --- a/src/math/GameMath.cpp +++ b/src/math/GameMath.cpp @@ -26,9 +26,10 @@ void move(float *x, float *y, model::Direction direction, float distance) void moveTowards(float *x, float *y, float towardsX, float towardsY, float speed) { - float angle = atan2f(towardsX - *x, towardsY - *y); + // Y coordinate is opposite of trigonometric coordinates + float angle = atan2f(-(towardsY - *y), towardsX - *x); *x += cosf(angle) * speed; - *y += sinf(angle) * speed; + *y -= sinf(angle) * speed; } float distanceSq(float x0, float y0, float x1, float y1) diff --git a/src/resources/ResourceManager.cpp b/src/resources/ResourceManager.cpp index 9af87a6..5b94022 100644 --- a/src/resources/ResourceManager.cpp +++ b/src/resources/ResourceManager.cpp @@ -114,6 +114,13 @@ void ResourceManager::loadGame() model::GameObject* item = model::GameObject::instantiate(prefab, "inv seed", player); inventory->add(item); } + + // Create collision qtree + auto mapObjIt = root->findByComponent(); + Assert (mapObjIt != root->childrenEnd(), "Can't find map!"); + components::Map* map = (*mapObjIt)->component(); + utils::RectF colliderArea(0, 0, map->width, map->height); + GameState::current().colliderTree = new utils::QTree(colliderArea); } std::string ResourceManager::getPath(ResourceId resourceId) diff --git a/src/storage/Parsers.cpp b/src/storage/Parsers.cpp index 0bbdc4b..d90808a 100644 --- a/src/storage/Parsers.cpp +++ b/src/storage/Parsers.cpp @@ -114,6 +114,25 @@ components::basic::Camera* parse (boost::property_tre return camera; } +template <> +components::basic::Collider* parse (boost::property_tree::ptree& root) +{ + if (root.size() > 0 && root.front().first == "Collider") + root = root.front().second; + + components::basic::Collider* collider = new components::basic::Collider(); + collider->solid = root.get(".solid", false); + + utils::RectF collisionRect; + collisionRect.x = root.get(".x", 0.0f); + collisionRect.y = root.get(".y", 0.0f); + collisionRect.w = root.get(".w", 1.0f); + collisionRect.h = root.get(".h", 1.0f); + + collider->collisionRect = collisionRect; + return collider; +} + template <> components::basic::Frame* parse (boost::property_tree::ptree& root) { @@ -461,6 +480,23 @@ model::Direction parseDirection(std::string directionStr) return direction; } +template <> +components::npc::MovingNpcController* parse (boost::property_tree::ptree& root) +{ + // Ensure we are on the correct node (property tree seems to add root of its own) + if (root.size() > 0 && root.front().first == "MovingNpcController") + root = root.front().second; + + components::npc::MovingNpcController* npc = new components::npc::MovingNpcController(); + + // Movement + std::string direction = root.get(".facingDirection", "South"); + npc->facingDirection = parseDirection(direction); + npc->walking = root.get(".walking", false); + + return npc; +} + template <> components::player::Player* parse (boost::property_tree::ptree& root) { @@ -560,6 +596,9 @@ model::GameObject* parse (boost::property_tree::ptree& root) else if (child.first == "Camera") gameObj->addComponent(parse(child.second)); + else if (child.first == "Collider") + gameObj->addComponent(parse(child.second)); + else if (child.first == "Grid") gameObj->addComponent(parse(child.second)); @@ -607,6 +646,10 @@ model::GameObject* parse (boost::property_tree::ptree& root) else if (child.first == "Seed") gameObj->addComponent(parse(child.second)); + // Components::npc + else if (child.first == "MovingNpcController") + gameObj->addComponent(parse(child.second)); + // Components::player else if (child.first == "Player") gameObj->addComponent(parse(child.second)); diff --git a/src/storage/Parsers.h b/src/storage/Parsers.h index 714e11a..5e23d15 100644 --- a/src/storage/Parsers.h +++ b/src/storage/Parsers.h @@ -9,6 +9,7 @@ #define STORAGE_PARSERS_PARSERS_H_ #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +51,9 @@ namespace storage { template <> components::basic::Camera* parse (boost::property_tree::ptree& root); + template <> + components::basic::Collider* parse (boost::property_tree::ptree& root); + template <> components::basic::Frame* parse (boost::property_tree::ptree& root); @@ -109,6 +114,9 @@ namespace storage { template <> components::plants::Seed* parse (boost::property_tree::ptree& root); + template <> + components::npc::MovingNpcController* parse (boost::property_tree::ptree& root); + template <> components::player::Player* parse (boost::property_tree::ptree& root); diff --git a/src/utils/QTree.h b/src/utils/QTree.h index f4a6eb7..d330781 100644 --- a/src/utils/QTree.h +++ b/src/utils/QTree.h @@ -9,6 +9,7 @@ #define UTILS_QTREE_H_ #include +#include #include #include @@ -58,7 +59,7 @@ namespace utils { /** * Quad tree */ - template + template class QTree : public INonAssignable { public: @@ -74,11 +75,13 @@ namespace utils { bool empty() const; size_t size() const; - void insert(T element, float x, float y); + void insert(const T& element, float x, float y); + void move(iterator it, float newX, float newY); void erase(iterator it); void clear(); - iterator find(T element); + iterator find(const T& element); + iterator find(const T& element, float x, float y); iterator lower_bound(const RectF& area); iterator upper_bound(const RectF& area); @@ -312,7 +315,7 @@ namespace utils { } template - void QTree::insert(T element, float x, float y) + void QTree::insert(const T& element, float x, float y) { Assert(m_bounds.contains(x, y), "Can't add element outside bounds."); @@ -344,6 +347,54 @@ namespace utils { } } + template + void QTree::move(iterator it, float newX, float newY) + { + Assert(!it.m_tree->m_isSplit, "Container modified."); + Assert(it.m_item < it.m_tree->m_itemsCount, "Container modified."); + + // Find destination tree + QTree* destTree = this; + bool found = false; + do + { + // Go to parent + if (!destTree->m_bounds.contains(newX, newY)) + destTree = destTree->m_parent; + + // Go to child + else if (destTree->m_isSplit) + { + for (size_t i = 0; i < 4; i++) + if (destTree->m_children[i]->m_bounds.contains(newX, newY)) + { + destTree = destTree->m_children[i]; + break; + } + } + else + { + found = true; + } + + } while (!found && destTree != nullptr); + + if (destTree == nullptr) + THROW(InvalidArgumentException, "Iterator doesn't belong to this tree."); + + // No need to move + if (destTree == it.m_tree) + { + it->x = newX; + it->y = newY; + } + + // Need to move. Preform an 'erase' and an 'insert' + T dataTemp = it->data; + it.m_tree->erase(it); + destTree->insert(dataTemp, newX, newY); + } + template void QTree::erase(iterator it) { @@ -385,7 +436,7 @@ namespace utils { } template - typename QTree::iterator QTree::find(T element) + typename QTree::iterator QTree::find(const T& element) { for (auto it = begin(); it != end(); it++) if (it->data == element) @@ -394,6 +445,31 @@ namespace utils { return end(); } + template + typename QTree::iterator QTree::find(const T& element, float x, float y) + { + if (!m_bounds.contains(x, y)) + { + if (m_parent) + return m_parent->find(element, x, y); + return end(); + } + if (m_isSplit) + { + for (size_t i = 0; i < 4; i++) + if (m_children[i]->m_bounds.contains(x, y)) + return m_children[i]->find(element, x, y); + } + else + { + for (size_t i = 0; i < m_itemsCount; i++) + if (m_items[i].data == element && m_items[i].x == x && m_items[i].y == y) + return iterator(this, RectF(x, y, 0, 0), i); + } + + return end(); + } + template typename QTree::iterator QTree::lower_bound(const RectF& area) {