Elysian Shadows

Adventure Log, Items, Friendlies, Enemies, and Warps

It's back. Not too big of a deal. When I started ESGamma, I never reimplemented the Adventure Log, the font engine, or the GuiSystem. These are all back and are go. Here you can see my Debug output wrapped back to the log during the frenzy of loading a level.

[b]Items/Friendlies/Enemies[/b]

One of our goals has simply been to [i]get shit into the fucking engine[/i] then worry about giving them more functionality later. Pretty much every core "entity" is now implemented (collidables, sprites, friendlies, enemies, characters, playable characters, warps, etc). They don't necessarily do much more than simply sit around the map for the time being, but they are ready to have any extra functionality added via Lua and/or Marcel's level editor. Friendlies are ready to have conversations/movements attached to them. Enemies are ready to be scripted for the battle system. Items are ready to be interacted with (via the Physics System). 

You can also see Pritam's beautiful angel working perfectly (and looking sexy) in the engine.

Now once Marcel's Toolkit has matured a little more, you will be able to lay ANYTHING (including your own user-defined entities) whose characteristics and behaviors are ready to be scripted via Lua.

[b]Warps[/b]

Warps are the mechanism through which we link multiple areas/levels together. These are components that may be attached to an entity. They contain a location, an ending location, an ending area, and an ending level. Some warps move the player around the same map. Some require the engine to load another level/area. This functionality was actually moderately tricky with the new PartySystem. Since the current area is the object that every subsystem is updating/operating on, the PartySystem is responsible for removing all party members from the area before the LevelSystem deallocates it to load a new area. It happens essentially like:

WarpSystem detects a warp.

WarpSystem tells the LevelSystem to change levels.

LevelSystem tells PartySystem to remove party members from the current level.

LevelSystem deallocates current level (allows up to 3 to remain in RAM ONLY on the PC version).

LevelSystem loads new level.

LevelSystem tells PartySystem to readd party members to level.

Control returns to the WarpSystem which returns control to the rest of the engine which continues execution normally.

In the first screenshot, the "warp" is being rendered as a white rectangle (which is toggle-able via Lua) so that you can see where it is located on the map. It takes you to a completely different level/area.

[b]CollisionSystem/Component and Optimization[/b]

Part of being able to implement the warp system required me to reimplement entity vs entity collision. This is handled by the CollisionSystem. Essentially the CollisionSystem checks each "Collidable" component for collision against each other collidable component. This is O(n^2) (moderately shitty) time complexity. I would rather not have to waste a week or two of good development time researching/implementing broad-phase collision until I HAVE TO. Without broad-phase collision being implemented (quad trees, BSPs, sweep and prune techniques), O(n^2) is the best you can get.

HOWEVER, I thought of a clever technique to optimize it further. Not every component REALLY needs to be checked against every other. Warps don't give a fuck if they collide with items. Items don't give a fuck if they collide with nonplayable characters. Playable Characters essentially give a fuck about colliding with everything. I have decided to give each collidable component its own set of collision flags:

struct CollisionFlags { 	CollisionFlags(); 	static const unsigned int SPRITE; 	static const unsigned int COLLIDABLE; 	static const unsigned int CHARACTER; 	static const unsigned int RIGID_BODY; 	static const unsigned int ITEM; 	static const unsigned int WARP; 	static const unsigned int FRIENDLY; 	static const unsigned int ENEMY; 	static const unsigned int PLAYER; 	static const unsigned int ALL; 	unsigned int checkAgainst; 	unsigned int type; }; const unsigned int CollisionFlags::SPRITE     = 1; const unsigned int CollisionFlags::COLLIDABLE = 2; const unsigned int CollisionFlags::CHARACTER  = 4; const unsigned int CollisionFlags::WARP       = 8; const unsigned int CollisionFlags::RIGID_BODY = 16; const unsigned int CollisionFlags::ITEM       = 32; const unsigned int CollisionFlags::FRIENDLY   = 64; const unsigned int CollisionFlags::ENEMY      = 128; const unsigned int CollisionFlags::PLAYER     = 256; const unsigned int CollisionFlags::ALL        = 0xFFFF; 

So now each collidable contains its own type and the type of collidables it needs to be checked against. These are essentially represented as bit flags in an unsigned integer (notice the powers of two). For the example, the first bit represents sprites, the second collidables, third characters, etc. So now, we are able to add a BITWISE COMPARISON between these two flags to our O(n^2) loop to rid us of unnecessary collision checks:

for(unsigned int i = 0; i < _curArea->collidable.size(); ++i) { 	for(unsigned int j = i+1; j < _curArea->collidable.size(); ++j) { 		if(_curArea->collidable[i ]->flags.checkAgainst & _curArea->collidable[j]->flags.type || 		  _curArea->collidable[i ]->flags.type & _curArea->collidable[j]->flags.checkAgainst) { 			if(BoundingBoxCollision(*_curArea->collidable[i ], *_curArea->collidable[j], contact)) { 				contact.body[0] = _curArea->collidable[i ]; 				contact.body[1] = _curArea->collidable[j]; 				addContact = new CollisionContact(contact); 				_curArea->collidable[i ]->_contact.push_back(addContact); 				_curArea->collidable[j]->_contact.push_back(addContact); 				_curArea->contact.push_back(addContact); 			} 		} 	} } 

Since an integer is represented as 4 bytes, this also gives us plenty of bits to define our own entity "types" for collision against. These bits may also be manipulated at run-time via Lua, so that you can randomly decide "Hey, I think my warp actually does give a fuck if it collides with an item."

If you have a look at the first screenshot, you will notice the translucent blue boxes. These are the bounding regions for each collidable component (playable/character components REQUIRE collidable components). We are able to define their relative positions and sizes as well (notice how party members 1's is at his feet). When these collide with other collidable components, the components are rendered as red. This is also toggle-able via Lua so that you can see what exactly is going on.

In the second screenshot, every character there is within your party (demonstrating that ANY character can be pushed into it, playable, friendly, enemies). Note how the first three characters have red collidable regions when they are touching the items. They are "playable" and their collision flags include items. The next three (character, enemy, friendly) have collision flags that don't care about items, so they remain blue.

[b]TerrainSystem Culling[/b]

Something that DESPERATELY needed to be implemented was some form of polygon culling in the map rendering routine. Nobody bitched about it yet, but if you were to take a 200x200 level and run it from Marcel's editor, the engine would literally run at 2fps... it would absolutely murder the Dreamcast. The problem was that we supported scale/zoom with our CameraSystem, so it wasn't a matter of easily adjusting a loop. I finally sat down and worked the math out. It works like a charm.

int startX = (cameraSystem->getFollowEntity()->transform.getPosition().x-(320.0f/cameraSystem->getScale().x))/32-2; int startY = (cameraSystem->getFollowEntity()->transform.getPosition().y-(240.0f/cameraSystem->getScale().y))/32-2; int endX = (cameraSystem->getFollowEntity()->transform.getPosition().x+320.0f/cameraSystem->getScale().x)/32+4; int endY = (cameraSystem->getFollowEntity()->transform.getPosition().y+240.0f/cameraSystem->getScale().y)/32+4; if(startX < 0) startX = 0; if(startY < 0) startY = 0; if(endX > _curArea->width) endX = _curArea->width; if(endY > _curArea->height) endY = _curArea->height; _curLevel->tilesheet.transform.setSize(32.0f,  32.0f); _curLevel->objectsheet.transform.setSize(32.0f,32.0f); _curLevel->tilesheet.transform.setScale(1.0f, 1.0f); _curLevel->objectsheet.transform.setScale(1.0f, 1.0f); for(unsigned int j = startY; j < endY; ++j) { for(unsigned int i = startX; i < endX; ++i) { 

You can see the beginning and ending tile coordinates as well as how many passes are required to render the map in each of the above screenshots. The Dreamcast will be most pleased.

As you can see, we aren't fucking around here. I'm going to be posting my plans for this coming week in "The Next Step." I will also be getting Mac/Windows/Linux/Dreamcast/PSP builds of this in the Project repository ASAP (have to reformat my Windows 7 PC thanks to Kendall).

Discussion Topic

Falco Girgis
Falco Girgis is the founder and lead software architect of the Elysian Shadows project. He was previously employed in the telecom industry before taking a chance on Kickstarter and quitting his job to live the dream. He is currently pursuing his masters in Computer Engineering with a focus on GPU architecture.