Secretchronicles / TSC

An open source two-dimensional platform game.
https://secretchronicles.org/
GNU General Public License v3.0
204 stars 50 forks source link

Replace m_type with pure C++ type information? #44

Open Quintus opened 10 years ago

Quintus commented 10 years ago

Currently, each sprite has a member named m_type that denotes what type of sprite it is, e.g. TYPE_FURBALL or TYPE_PARTICLE_EMITTER. In other words, this duplicates the information already present in the C++ type system itself. For example, a check like this:

if (p_sprite->m_type == TYPE_FURBALL)
  // Do things

can be replaced with the following equal code that relies on C++’s type system instead (yes, all furballs have m_type set to TYPE_FURBALL in their constructor):

if (dynamic_cast<cFurball>(p_sprite))
  // Do things

This also has the advantage we can check for intermediate classes like cEnemy instead of having to list all enemy types existing in SMC.

However, I’m not sure where these m_type checks are done in SMC’s code. dynamic_cast is AFAIK a more expensive operation, i.e. it doesn’t perform as good as a simple integer check does (m_type is of type SpriteType, which is an enum, which is an integer). If such checks happen somewhere in SMC’s Update() or Draw() functions, they are called on every (!) frame, which would make a switch to dynamic_cast probably impossible. This needs to be checked; however, if there aren’t such highly frequented checks, we should probably remove SpriteType entirely and completely rely on C++’s type system.

Valete, Quintus

Luiji commented 10 years ago

I believe the only reason we'd use type data every loop is for collision detection (e.g. did I collide with an enemy and thus have to kill myself?). This could be eliminated by rearchitecturing the collision detection system. The current way it seems to work is that it checks for every possible collision and registers them with all applicable objects, then lets them deal with them in a separate update loop. This is not very efficient, as there are many cases where collision information is ignored.

Something I've done in my personal projects that is very effective is to instead have external objects manually check their own collisions, so they can check against only relevant objects. This can be further expedited by having objects register themselves with relevant arrays.

For instance, all massive objects register themselves in the Massive Objects array. Whenever a massive-effected object moves, it checks its collision against the Massive Objects array to see how far it can go before collision. This reduces the number of things it has to check against and removes the need to run type checking, since only relevant objects are in the array.

Furthermore, enemies all check themselves against the Player Objects array, and if they detect collision, kill the player. The advantage of having a Player Objects array instead of an Enemy Objects array is that it simplifies the player code by moving the antagonistic code into the relevant enemies, who can determine for themselves how much and when to damage the player, and also because the Player Objects array is going to be much shorter (in practice, there will only be one player, so it technically won't even be an array).

This mechanism can eliminate the need for type information for anything other than serialization, where the inefficiencies of RTTI are acceptable, while also generally optimizing the collision architecture.

Quintus commented 10 years ago

When I implemented the Lava stuff today I had to get may head around SMC’s collision system as well. The truth is, SMC does not use m_type for collision checking -- it does this with m_sprite_array, which is probably misnamed. This is why I did not make the Lava a new "massive type", but rather a new "sprite array".

Second, collisions are always fired on the object moving into another object. The moving object then calls Send_Collision() to delegate collision handling to its collision partner if it doesn’t want to cope with the collision itself; this is how all the enemies work for example. cLevel_Player::Handle_Collision_Enemy() first checks for stuff like star[1], then calls Send_Collision() to re-fire the collision on the respective collision partner, invoking for example cFurball::Handle_Collision_Player(), so that the actual damaging is done in the enemy’s code.

Rewriting SMC’s collision system is probably a good idea. I’ve yet to look at how exactly SMC’s mainloop is arranged, but I guess some performance tuning will most likely be good for it. Not sure if we should aim for this in version 2.0.0.

Vale, Quintus

[1]: Fun fact: This means if the enemy runs into you, star is not checked. The enemy will just go past you, without anything happening. Try it!

Quintus commented 10 years ago

Just ran in this limitation while implementing feature #34. The loose shell is a cTurtle subclass, any all the collision code created for collision with turtles is exactly identical for loose shells. But as this code always checks whether m_type == TYPE_TURTLE all this code has to be changed to also check for m_type == TYPE_SHELL, which is nonsense. TYPE_SHELL is a TYPE_TURTLE, just with some stuff added on top.

Vale, Quintus