Showing posts with label core. Show all posts
Showing posts with label core. Show all posts

Sunday, February 06, 2011

platforms (at last ?)

1. The platform must be passive
This is due to the fact the platform could be carrying several objects and must remain "unaware" of this.

2. Having a "reference GOB" is fundamental
Not only walking on a platform involves Bilou (or other "character" GOBs) to have controllers that look at the behaviour of other GOBs. It also happens with any other kind of vehicles, with homing shots that have per-instance (as opposed to class-wide) target, or to carrying of objects/monsters. Moreover, it is linked to a collision area of the GOB rather than to the GOB itself.



Bion. Refaisons d'abord le point sur les faits:
  1. les plate-formes doivent être passives;
  2. un GOB peut toujours avoir un GOB de référence
  3. le script ne doit être nécessaire que pour les changements d'état.
  4. l'état propre aux contrôleurs est partagé par tous les personnages qui utilisent ce contrôleur.

3. Collisions are only needed when we start/stop being transported.
We simply need to exit the "falling" state when a platform is hit and to leave the "walking/idle" state when the platform stops being solid. To "keep walking" on an established platform, no "event" is required, and no "scriptable action" must take place.

4. It is preferable not to mess with controllers chain
Although it was my initial idea to have collision with platforms to prepend a "walk-on-platform" micro-controller on Bilou, it only make things harder to work with. For a start, controllers are essentially class-wide resources (as the whole state machine), meaning that it would be hard to have e.g. applemen properly walking on platforms this way. Second, it requires that we identify the location in the controllers chain where the new controller should be added, which we currently have no support for. Finally, it would be impossible to insert controller-specific event for "on-platforms" micro-controller, because it is unknown at the "script compilation" time.


Pour pouvoir implémenter proprement les plate-formes mobiles, il faut d'abord:
  • que les plate-formes se soient toutes déplacées avant que l'on ne commence les calculs de mouvement des personnages succeptibles de marcher dessus.
  • que les flags de collisions permettent d'indiquer "conserver ma référence après la collision" et "accepter une référence suite à la collision"
Si tout ce passe bien, une fois ces éléments en place, il sera non seulement possible de créer des plate-formes mais aussi tout autre type de "déplacement aligné" simplement par ajustement du script (quelles collisions, à quel moment, par rapport à quelle zone, avec quel effet sur les contrôleurs, etc.)

The only modifications required on the engine should thus be 1) that GOBs acting as mobile platforms are "animated" prior to GOBs that walk on them (to avoid zool-lags) and 2) allow collision flags to indicate whether a reference to the passive object should be kept and whether the active object is willing to change its current reference.

It is still unclear, however, to what extent platforms require their own collision cast (that is, beyond HERO and EVIL).

Monday, June 01, 2009

Les collisions

Les collisions, c'est probablement un des éléments les plus important de la gestion des sprites dans un moteur de jeu. Outre l'aspect purement technique "y a-t-il ou pas collision" et l'aspect d'optimisation "comment tester les collisions entre N sprites (potentiellement (N*N-1)/2 calculs) en un temps raisonnable", il y a le côté "logique du jeu": comment vont réagir les différents objets en cas de collision.

Mes premiers jeux étaient assez élémentaires de ce point de vue là: collision = touché sauf si ybilou+8<ymonster. une tentative un peu simpliste de simuler le "pogotage de cafetière à la SuperMario".
Mais ça, c'était du temps du BASIC. En 1997 (déjà programmeur en assembleur et bricolant mon 'mod player à l'époque), je suis à l'unif le cours d'algorithmique de PaDM ou je découvre les joies des listes liées et où SJ me guide tout doucement vers la compréhension que "tes 'registres de sprites' ressemblent à des variables-membre en POO".

Collisions play a major role in game engines. So far, in my former game attempts, i mostly focused on "how do we know there is a collision?" or "how can i support many sprites without slow-down due to O(N²) collisions detection effort?". This time, i'm rather focusing on "what shall we do once a collision is detected?". Collisions are event that will affect the state machine of our sprites... of both sprites that are involved in the collision. My first "serious" design effort on collisions management dates back from "Out'm'Up" game for the 100K-game competition at Inscene'2K (a demoparty in Belgium), the distilled wisdom resulting on my effort to build an "Ultimate Game Maker" between '97 and '99.

Rétrospectivement, ça fait un peu peur: après 10 ans de programmation, je n'avais pas encore terriblement évolué dans ma manière d'approcher les problèmes de quand je programmais mon "Calimero" en BASIC C64 à grand coup de Gotos. Seule grosse différence, je tentais de reproduire en Software (via des "registres" pour la position, la vitesse, la puissance, etc.) le hardware idéal pour mon moteur de jeu. Un variante non-déclarée de la machine virtuelle donc, mais qui me poussait à sur-définir des éléments tout à fait accessoire du genre "combien de bits pour le niveau d'attaque et le niveau de défense du sprite ?"

Out'm'Up, mon dernier shoot en date
Bref, les années "Ultimate Game Maker" sont loin, maintenant. Si je reprends de temps en temps la farde bleue à carreau où tout cela est consigné, c'est plus par nostalgie que pour son contenu. Ou pour dater une technique ancestrale. En juin 2000, par exemple, j'expérimente dans "Out'm'up" une technique dont je ne me séparerais plus: les castes de sprites. Dans toute collisions, il y a toujours un sprite actif (qui cherche la bagarre) et un sprite passif (qui subit la collision). Le sprite passif appartient à une des deux castes possibles (hero ou evil) et le sprite actif ne cherchera pour ses collisions que dans la liste de la caste correspondance. En clair, celà signifie que les tirs de mon p'tit vaisseau ne testeront jamais que les "ennemis" (et pas les autres tirs ou les bonus) et que les ennemis ne se testent pas entre-eux (mais uniquement vis-à-vis du joueur). L'un dans l'autre, la technique s'est montrée tout à fait satisfaisante vu la quantité d'objets à l'écran (on est encore loin d'un Bullet Hell, bien sûr, mais n'empèche).

Active/Passive
The core concept is that in a collision between two sprites, one is *active* and the other *passive*. I.e. the passive sprite has just registered itself in a list of "sprites that accept collisions", but the active sprite is the one who will scan that list for a match. Together with that mechanism comes the idea of *casts*. We only have a limited number of such "lists" where sprites can register -- one per sprite cast. And so far, in all games two casts seems to be enough: heroes and evils. In a laser-ufo collision, for instance, the UFO is passive evil and the laser is active. That means that the laser only checks UFOs for collisions, not other lasers or explosions, bonuses, etc. In the UFO-spaceship collision, the spaceship is passive hero and the UFO is active. You'll note that the cast of the active sprite is irrelevant in a collision. So far, it has proven much more efficient and flexible than adjusting "power levels" (in RSD Game-Maker, all sprite had a power level, and when the collide, the one with the highest level kills the other one. period)

Si ça peut paraître un peu annecdotique dans un jeu de plate-formes, ça n'en reste pas moins la base de la gestion des collisions dans ma dernière démo. J'y ai ajouté le système des masques inspiré du code de Jill of the Jungle qui, une fois que j'aurai bricolé l'évaluateur d'expressions, permettra de faire réagir les personnages différemment selon la "source" de la collision. Petit exemple ici avec le "pendat" et ses réactions possible en cas de collision avec un objet lancé, avec un encrier bloquant ou avec le personnage.

In a platforming game like Bilou, this needs to be extended. Not only the cast of the 'hitter' is important, but also its nature, which i intend to implement through collision flags, drawing inspiration from Jill of the Jungle source code. Every "active area" defines a set of flags that identify "what it is" while "passive collision areas" indicate "what they are sensitive to". You can then have a penguin monster that takes a single hit unless it is hit by F_FIRE, which kills it instantaneously.

While the cast of a sprite never changes, each state can define various areas, with different flags. That allows us to have e.g. a special "strike" move with an additional, powerful attack area or a move that unveils a weak point. The game script defines transition on a per-area basis, so what happens when you hit one weak point can be different than what happens when you hit another area. Now, i still have to "make it so" in the code, and make the result of "area masks" visible to my "GobExpressions". In an attempt to separate the concerns, each sprite will take care of its own state manipulation: we won't have the pencil 'killing' anyone directly, but i expect that i might need some information about "the other guy" anyway (relative position, speed, etc.)

J'aurai donc bientôt réglé le bug qui "blesse" Bilou à chaque fois qu'il ramasse un bonus et un état dédié 'blessé' me permettra de rendre plus facilement Funky Funghi infranchissable. Par contre, rendre certains ennemis "solides", même quand Bilou est invulnérable temporairement, ça reste un challenge.

2024 #choice reality check: Well, now that I have access to countless hitbox reveal (mostly by Upsilandre), it turns out that having separate active and passive boxes (typically called hitbox and hurtbox) is the de-facto solution. Having a separate list for player and foes also is. But the more I progress, the more it shows that additional casts will be required for bridges and things alike. Nothing has been coded so far, though.

Friday, February 27, 2009

World Collisions

Left alone, testpoinst are not really a satisfactory way to handle collisions between objects of a game and the world.
First they don't cover enough and cannot catch all the odd situations. Testpoints are points and they cannot detect the case where you're jumping through a corner unless you have many of them (up to one per tile your character covers).


Je ne suis plus trop emballé par les tests-points tels que je les avait décrit précédemment. En premier, ils ne capturent suffisamment toutes les collisions. A moins de les multiplier, on risque toujours d'avoir une partie du personnage qui rentre dans un coin de mur lors d'un mouvement, et en donnant à chaque état sa propre liste de testpoints, on ne fait qu'augmenter les possibilités de bugs.

Second, they are only boolean. They don't let you express that a fish can only move into water, that a bird can only move in air, and that a frog can move through both, but that only the ghost can also move through walls (and i'm sure we can come up with a negative space creature that can only walk through/over walls). This is a situation much more common that one could think. As soon as you add ladders to your levels, you want to express that climbing the ladder "is only possible over ladders tiles". That was a quite strong restriction of Recreational Game Maker where the only way you could create a ladder for a platformer game was to set gravity=0 on ladder tiles ... As a result, your character zooms through ladders if you press the 'jump' key and miserably 'fail to climb up the air' if you press the 'upwards' when he's not over a ladder.

Le deuxième souci avec les test-points, c'est qu'ils sont simplement binaires. Alumés ou éteints. Point barre. Pas moyen de préciser qu'un poisson ne peut se déplacer que dans l'eau ou qu'un oiseau ne peut aller que dans l'air, mais que la grenouille peut évoluer dans les deux milieux. Si on y réfléchit bien, ce genre de situation apparaît bien plus souvent qu'on ne pourrait le penser. Le simple ajout d'échelles dans nos niveau oblige de définir que "escalader une échelle" ne peut s'envisager que s'il y a une échelle à escalader. Le Recreational Game Maker sur lequel j'ai développé (entre-autres) la série des badman ne permettait pas ce genre de subtilité: une échelle était tout simplement un bloc sans gravité, de sorte que l'on pouvait y monter en sautant -- à une vitesse franchement irréaliste -- et on aurait vu le personnage ridiculement patauger dans l'air en faisant de petits bonds si on avait appuyé sur la touche "monter à l'échelle" alors qu'il n'y avait aucune échelle.

The approach of "the nature of a tile" is thus interesting: it can express (through flags) who can do what over that tile. This is a concept used both in Xargon and (afaik) in the side-scrolling Rayman game, where you have "monster-walls" that monsters can't cross (though Rayman don't feel them). This is imho an elegant solution to many level design problems. Let's take the walking block of "Inside the Machine", who wanders around its initial location, "guarding" a virtual spot even if it could theoretically walk further. Morukutsu relied on a "distance" counter that is increased/decreased through the animation code, which won't let you alter the 'coverage area' of the ennemy through level design, but only through implementation ... not so fun.

L'approche "nature du tile", telle qu'elle est utilisée dans Xargon est intéressante à plus d'un titre, et je soupçonne les concepteurs de Rayman (la version 2D) d'avoir suivi une approche similaire. En particulier, elle permet l'introduction de "grillage à monstres", des blocs invisibles et non-bloquant pour le personnage et les projectiles, mais qui sont assimilés à un mur par certaines plates-formes mobiles et par certains monstres. Comme je l'avais déjà précisé lors de l'analyse des maps de Commander Keen 5, je trouve que c'est une technique élégante pour construire les niveaux. Si je compare à "Inside the Machine" (l'autre jeu dont j'ai étudié les sources pendant mes longues soirées d'hiver en Suisse), l'alternative consiste à programmer les monstres de sorte qu'ils ne s'éloignent jamais de plus de n pixels de leur position initiale, en maintenant une variable supplémentaire "distance" qui les force à faire demi-tour. C'est exactement le genre de "pré-câblage de code" que je cherche à éviter. Décider jusqu'où un monstre va, c'est une question de level design, pas de programmation du comportement des monstres.

The final argument is that some testpoints are typically more important than others, and there is a strong relationship between which testpoint is triggered and which movements you can do. A problem i have with the current implementation of collisions in Bilou is that as soon as one of the testpoint checks fails, the whole move is cancelled. If you fall along a wall, you don't expect your character to "stick" on the wall because you keep pressing the D-pad in the wall's direction. Yet, that's exactly what Bilou does right now.
Instead, we'd like something like

if cando(dx,dy) then (x,y) := (x+dx,y+dy)
elsif cando(0,dy) then (x,y) := (x,y+dy)
else { 
  (x,y) := (x,align_on_tile(y+dy)); 
  trigger_testfail_event 
}
where cando(deltax, deltay) checks the whole area covered by the character at a new temptative location. But still, you don't want such behaviour for all your monsters, and it's certainly something that is specific to jumping. You wouldn't see this ever in a Zelda game : instead you'd have the character "walk around" a block if he's sufficiently near to the edge of that block. So this logic is really controller-related rather than being something that belongs to the game engine itself.

So, do we still need any testpoint ? after all, Xargon's cando() used in controller-specific code seems to be fine. Actually, there's something that cando() cannot catch: mixed situations. The controller will be able to tell the game engine that it's no longer possible to fall, but it can't tell whether we are now landing, diving, or bouncing against a corner. This is where we *really* need test-points, and you'll notice that testpoints are only used for state transitions.

I still have to figure out the best way to convert my codebase to this new approach -- the fact that i'm busy working on the real-world house not really helping -- but i think it might be what i've been looking for since the start of the project.


Enfin, certains testpoints sont clairement plus important que d'autres, et le déplacement indiqué par le contrôleur doit être ajusté en fonction de la situation. J'ai le problème dans le saut de Bilou, actuellement: quelque soit le testpoint qui détecte une collision, le mouvement complet est annulé. Résultat, si Bilou rentre dans un mur lors d'un saut, il va rester "collé" au mur jusqu'à ce que je relâche la touche de direction. Celà n'arriverait pas si je pouvais donner une priorité, ou une description plus complète du rôle des différents testpoints (p.ex. puisque c'est un test-point "mur", il n'annulerait que le déplacement horizontal, etc.)
Mais en fait, ce genre de logique est commune à tous les déplacements guidés par la gravité dans un jeu de plate-forme. Ce n'est pas quelque-chose de spécifique au comportement de Bilou, et celà pourrait sans difficultés être introduit dans le contrôleur du sprite. En tout cas, ce n'est clairement pas à introduire dans la classe GameObject elle-même, puisque si on veut faire un r-type, un boulder-dash ou un zelda, on en aura pas besoin.

Mais alors, à quoi servent encore nos test-points ? eh bien, à décider de quel sera le prochain état lorsque le contrôleur signale qu'il n'est pas possible de continuer. Ca veut dire aussi que le contrôleur devra évidemment ajuster la position du sprite de manière à respecter les "cando()" mais pour que les testpoints puissent détecter quelque-chose quand-même :P
  • [done] extract persistent state (found in map and accessible by other classes)
  • [done] iGobPubstate::cando(x,y,flags)
  • [done] manipulate tile_flags array (that translate tile type into tile flags) from the script
  • [done] controllers report "fail" when no "cando()" can be applied
  • [done] who updates coordinates ?
  • [done] update GameObject::play() and trymove()

2024 #choice reality check: cando is still there. it has allowed slopes, jump-throughs and more. And I finally have proof that it also works with levels featuring water. Pfew!