Interesting, I've been dealing with this "permeability" concept just recently in my own game (with some personal success).
Here's what I've been doing:
My tiles are all represented by a "Tile" base class that has "dislodgeX" and "dislodgeY" methods, a hitbox and any flags I want within it.
Every time I move an entity, I do an equivalent to the following:
void Entity::move (const sf::Vector2f& delta) {
moveHitbox(delta.x, 0);
for (Tile& tile: getStage().tiles)
tile.dislodgeX(*this, delta.x);
moveHitbox(0, delta.y);
for (Tile& tile: getStage().tiles)
tile.dislodgeY(*this, delta.y);
}
And a Tile's ::dislodgeX method, for example, looks somewhat like the following:
void Tile::dislodgeX (Entity& ent, float delta) const {
if (!ent.getHitbox().intersects(this->getHitbox())) return;
// ---
if (delta < 0) {
// Entity is moving left. [T]<-[E]
ent.setHitboxLeft(this->getHitbox().left + this->getHitbox().width);
// A similar method to the above makes it so that the entity and the tile's
// hitboxes are now perfectly adjacent. [T|E]
}
else if (delta != 0) {
// Entity is moving right [E]->[T]
ent.setHitboxLeft(this->getHitbox().left - ent.getHitbox().width);
// [E|T]
}
}
How could we implement permeability? What I did is somewhat complex or very simple, it depends on how you look at it. My "Entities" have two std::set<const Tile*>'s within them that keeps track of the impermeable tiles in which they are inside (one set is for the previous tick, and another is for the new tick), and here is how make the Tiles use these sets:
struct Entity {
std::set<const Tile*> tilesInside, prevTilesInside;
/* ... */
void move (const sf::Vector2f&);
bool wasInside (const Tile&) const;
};
void Entity::move (const sf::Vector2f& delta) {
prevTilesInside = tilesInside;
tilesInside.clear();
/* Alternatively: prevTilesInside = std::move(tilesInside); */
// ---
moveHitbox(delta.x, 0);
for (Tile& tile: getStage().tiles)
tile.dislodgeX(*this, delta.x);
moveHitbox(0, delta.y);
for (Tile& tile: getStage().tiles)
tile.dislodgeY(*this, delta.y);
}
bool Entity::wasInside (const Tile& tile) const {
return prevTilesInside.find(&tile) != prevTilesInside.end()
|| tilesInside.find(&tile) != tilesInside.end();
}
/*
...
*/
void Tile::dislodgeX (Entity& ent, float delta) const {
if (!ent.getHitbox().intersects(this->getHitbox())) return;
// ---
if (delta < 0) {
if (!this->allowsEnteringLeft && !ent.wasInside(*this)) {
ent.setHitboxLeft(this->getHitbox().left + this->getHitbox().width);
}
else ent.tilesInside.insert(this);
}
else if (delta != 0) {
if (!this->allowsEnteringRight && !ent.wasInside(*this)) {
ent.setHitboxLeft(this->getHitbox().left - ent.getHitbox().width);
}
else ent.tilesInside.insert(this);
}
}
I tried many methods before this one, each more convoluted than the other, but this was what worked best. It's effective. And why sets instead of a single pointer, you may ask? The entity can always be inside more than one tile at once.
Good luck with your project! I wouldn't recommend using physics engines, indeed, at least not for a platformer that requires tight controls - or, mainly, for learning. As long as you keep your game timestep-fixed (constant framerates) and you stick to using AABB rectangles for collision, you won't need them. But it's not like you won't have some work to do, mainly when dealing with accelerations and gravity. It's either "stick to a engine and cut off work" or "write it up yourself and have complete control over the physics".