Release Forget Me Not - new shooter/maze game from Nyarla Labs


Wrote a little something about this game because it just keeps getting better and better:


http://ithic.com/drupal1/node/33


Let me know if I should add anything or change anything.

Hey Link, thanks for the lovely review! Glad you're liking FMN.


And thanks again to Dunny for going above and beyond the call of duty with this port... the new challenge level stuff was some experimental code I had left disabled in the source, because I wasn't happy with it yet. Dunny had some great ideas on how to make it play better and so turned it back on and modified it... no other versions have those challenge levels yet. 8)
 
@nyarla im just glad you made the game :) its really nice and i love the retro touch you gave it (visuals, sound fx and gameplay).


truly brilliant.
 
I'm fully in love with this game. I'm playing it so much that all I see is pixels when I close my eyes at night. ;)
 
Been playing it mostly on iOS but today I dusted off my Pandora and have been bashing away at it in all its glory ..
 
Been playing it mostly on iOS but today I dusted off my Pandora and have been bashing away at it in all its glory ..

How's the performance compared to the iOS version? The PC version is massively faster by a factor of about 1.5 on my PC... And I have no idea how to speed it up on the Pandora :(


D.
 
It seems to me to be quite performant on iOS (iPhone4/iPad2) and .. a little laggy sometimes, I dunno if its just frame adjustment or what, on Pandora .. not terrible, but definitely not as 'smooth' all-around as on the iPad2. However, the new modes in the Pandora version make up for it .. ;)
 
Been playing it mostly on iOS but today I dusted off my Pandora and have been bashing away at it in all its glory ..

How's the performance compared to the iOS version? The PC version is massively faster by a factor of about 1.5 on my PC... And I have no idea how to speed it up on the Pandora :(


Edit: Actually, I do and it involves me learning how to use pThreads to remove the quad buffer filling and rendering from the game loop, but as my development environment consists of a non-networked Pandora, a Debian VM, Yactfeau, GEdit and a Terminal, it's not much fun. I really miss my RADStudio IDE in windows :(


D.
 
It seems to me to be quite performant on iOS (iPhone4/iPad2) and .. a little laggy sometimes, I dunno if its just frame adjustment or what, on Pandora .. not terrible, but definitely not as 'smooth' all-around as on the iPad2. However, the new modes in the Pandora version make up for it .. ;)

The problem is that every time the screen is updated, each tile is sent to the display. Then each sprite (including the dots) is updated and sent. Sending involves building a "quad buffer" of textured polygons which GLES then draws. For small chunky mode maps that's fine (though still not as fast as the PC version) but when the maps get larger the whole game slows down as each frame takes longer to process.


I removed the GLES renderer and it still crawls (though is black). I removed the quad-buffering and I'm suddenly up at 200fps+ - but again it's all black :)


If I can sort out the quad buffering (or multi-thread it) then performance will be similar to the PC version.


It certainly does seem that even clocked at 900MHz+, our little Pandora just isn't as fast as an iPhone 3GS running iOS - the codebase for FMN proves it, as it's almost identical to the iOS version, save for front-end stuff.


D.
 
Last edited by a moderator:
does it convert a sdl_surface to a texture?


For VVVVVV it used a quad texture, what i had to do was create a power by 2 texture i.e 512x256. Load the data into the texture using subtex api. Then adjust the texcoords to only sample the 320x240 area of the texture.
 
does it convert a sdl_surface to a texture?


For VVVVVV it used a quad texture, what i had to do was create a power by 2 texture i.e 512x256. Load the data into the texture using subtex api. Then adjust the texcoords to only sample the 320x240 area of the texture.

SDL is only used to create the screen, after that it's all EGL/GLES calls - no SDL surfaces are used in the renderer. The main texture is loaded with SDL_Image to a SDL surface and then freed. With just the one texture, the quads are set to u/v from that.


Here is the code to create a quad - it's called by all procs that need to draw to the screen - maze walls, flowers, items and enemies:



Code:
void GLInterface::DrawImage( unsigned int layerNum, unsigned int size, unsigned int frame, float x, float y, float hScale, float vScale, float angle, const TColour* colour )

{

assert( layerNum < _dynamicLayers.size() );


y = _h - y;


float w = frameSizes[ size ] * hScale;

float h = frameSizes[ size ] * vScale;


float xOfs = w * 0.5;

float yOfs = h * 0.5;


TRect quad;

quad.x[ 0 ] = x - xOfs;

quad.y[ 0 ] = y - yOfs;

quad.x[ 1 ] = quad.x[ 0 ] + w;

quad.y[ 1 ] = quad.y[ 0 ] + h;


TRect* texRect = &_textures[ _dynamicLayers[ layerNum ]->textureNum ]->texRects[ size ][ frame ];

const TColour* destColour = &_white;

if (colour) { destColour = colour; }


if ( angle == 0.0 )  // add non-rotated quad

{

_dynamicLayers[ layerNum ]->buffer->AddQuad( quad, *texRect, *destColour );

}

else  // add rotated quad.

{

// all this stuff is what rotates the corners. probably a way faster way to do it.. affine matrices? i dunno

float newX[ 4 ];

float newY[ 4 ];


newX[ 0 ] = cosf( angle ) * ( quad.x[ 0 ] - x ) - sinf( angle ) * ( quad.y[ 0 ] - y ) + x;

newY[ 0 ] = sinf( angle ) * ( quad.x[ 0 ] - x ) + cosf( angle ) * ( quad.y[ 0 ] - y ) + y;


newX[ 1 ] = cosf( angle ) * ( quad.x[ 1 ] - x ) - sinf( angle ) * ( quad.y[ 0 ] - y ) + x;

newY[ 1 ] = sinf( angle ) * ( quad.x[ 1 ] - x ) + cosf( angle ) * ( quad.y[ 0 ] - y ) + y;


newX[ 2 ] = cosf( angle ) * ( quad.x[ 0 ] - x ) - sinf( angle ) * ( quad.y[ 1 ] - y ) + x;

newY[ 2 ] = sinf( angle ) * ( quad.x[ 0 ] - x ) + cosf( angle ) * ( quad.y[ 1 ] - y ) + y;


newX[ 3 ] = cosf( angle ) * ( quad.x[ 1 ] - x ) - sinf( angle ) * ( quad.y[ 1 ] - y ) + x;

newY[ 3 ] = sinf( angle ) * ( quad.x[ 1 ] - x ) + cosf( angle ) * ( quad.y[ 1 ] - y ) + y;


_dynamicLayers[ layerNum ]->buffer->AddQuad(newX[ 0 ], newY[ 0 ], newX[ 1 ], newY[ 1 ], newX[ 2 ], newY[ 2 ], newX[ 3 ], newY[ 3 ], *texRect, *destColour );


}


}



And the AddQuad() call is this:





Code:
void RenderBuffer::AddQuad( const TRect& quad, const TRect& tex, const TColour& col )

{

if ( _numVerts <= _maxVerts - 4 )

{

int faceIndex = _numFaces * 3;


int vertIndex = _numVerts;

int x = 0;

int y = 0;

_pVertData[ vertIndex ].vert.x = quad.x[ x ];

_pVertData[ vertIndex ].vert.y = quad.y[ y ];

_pVertData[ vertIndex ].tex.x = tex.x[ x ];

_pVertData[ vertIndex ].tex.y = tex.y[ y ];

_pVertData[ vertIndex ].colour.r = col.r;

_pVertData[ vertIndex ].colour.g = col.g;

_pVertData[ vertIndex ].colour.b = col.b;

_pVertData[ vertIndex ].colour.a = col.a;


vertIndex++;

x = 1;

y = 0;

_pVertData[ vertIndex ].vert.x = quad.x[ x ];

_pVertData[ vertIndex ].vert.y = quad.y[ y ];

_pVertData[ vertIndex ].tex.x = tex.x[ x ];

_pVertData[ vertIndex ].tex.y = tex.y[ y ];

_pVertData[ vertIndex ].colour.r = col.r;

_pVertData[ vertIndex ].colour.g = col.g;

_pVertData[ vertIndex ].colour.b = col.b;

_pVertData[ vertIndex ].colour.a = col.a;


vertIndex++;

x = 0;

y = 1;

_pVertData[ vertIndex ].vert.x = quad.x[ x ];

_pVertData[ vertIndex ].vert.y = quad.y[ y ];

_pVertData[ vertIndex ].tex.x = tex.x[ x ];

_pVertData[ vertIndex ].tex.y = tex.y[ y ];

_pVertData[ vertIndex ].colour.r = col.r;

_pVertData[ vertIndex ].colour.g = col.g;

_pVertData[ vertIndex ].colour.b = col.b;

_pVertData[ vertIndex ].colour.a = col.a;


vertIndex++;

x = 1;

y = 1;

_pVertData[ vertIndex ].vert.x = quad.x[ x ];

_pVertData[ vertIndex ].vert.y = quad.y[ y ];

_pVertData[ vertIndex ].tex.x = tex.x[ x ];

_pVertData[ vertIndex ].tex.y = tex.y[ y ];

_pVertData[ vertIndex ].colour.r = col.r;

_pVertData[ vertIndex ].colour.g = col.g;

_pVertData[ vertIndex ].colour.b = col.b;

_pVertData[ vertIndex ].colour.a = col.a;



_pFaceData[ faceIndex + 0 ] = _numVerts + 0;

_pFaceData[ faceIndex + 1 ] = _numVerts + 1;

_pFaceData[ faceIndex + 2 ] = _numVerts + 2;

_pFaceData[ faceIndex + 3 ] = _numVerts + 1;

_pFaceData[ faceIndex + 4 ] = _numVerts + 2;

_pFaceData[ faceIndex + 5 ] = _numVerts + 3;


_numVerts += 4;

_numFaces += 2;

}

}


void RenderBuffer::AddQuad( const float x0, const float y0, const float x1, const float y1, const float x2, const float y2, const float x3, const float y3, const TRect& tex, const TColour& col )

{

if ( _numVerts <= _maxVerts - 4 )

{

int faceIndex = _numFaces * 3;


int vertIndex = _numVerts;

int x = 0;

int y = 0;

_pVertData[ vertIndex ].vert.x = x0;

_pVertData[ vertIndex ].vert.y = y0;

_pVertData[ vertIndex ].tex.x = tex.x[ x ];

_pVertData[ vertIndex ].tex.y = tex.y[ y ];

_pVertData[ vertIndex ].colour.r = col.r;

_pVertData[ vertIndex ].colour.g = col.g;

_pVertData[ vertIndex ].colour.b = col.b;

_pVertData[ vertIndex ].colour.a = col.a;


vertIndex++;

x = 1;

y = 0;

_pVertData[ vertIndex ].vert.x = x1;

_pVertData[ vertIndex ].vert.y = y1;

_pVertData[ vertIndex ].tex.x = tex.x[ x ];

_pVertData[ vertIndex ].tex.y = tex.y[ y ];

_pVertData[ vertIndex ].colour.r = col.r;

_pVertData[ vertIndex ].colour.g = col.g;

_pVertData[ vertIndex ].colour.b = col.b;

_pVertData[ vertIndex ].colour.a = col.a;


vertIndex++;

x = 0;

y = 1;

_pVertData[ vertIndex ].vert.x = x2;

_pVertData[ vertIndex ].vert.y = y2;

_pVertData[ vertIndex ].tex.x = tex.x[ x ];

_pVertData[ vertIndex ].tex.y = tex.y[ y ];

_pVertData[ vertIndex ].colour.r = col.r;

_pVertData[ vertIndex ].colour.g = col.g;

_pVertData[ vertIndex ].colour.b = col.b;

_pVertData[ vertIndex ].colour.a = col.a;


vertIndex++;

x = 1;

y = 1;

_pVertData[ vertIndex ].vert.x = x3;

_pVertData[ vertIndex ].vert.y = y3;

_pVertData[ vertIndex ].tex.x = tex.x[ x ];

_pVertData[ vertIndex ].tex.y = tex.y[ y ];

_pVertData[ vertIndex ].colour.r = col.r;

_pVertData[ vertIndex ].colour.g = col.g;

_pVertData[ vertIndex ].colour.b = col.b;

_pVertData[ vertIndex ].colour.a = col.a;


_pFaceData[ faceIndex + 0 ] = _numVerts + 0;

_pFaceData[ faceIndex + 1 ] = _numVerts + 1;

_pFaceData[ faceIndex + 2 ] = _numVerts + 2;

_pFaceData[ faceIndex + 3 ] = _numVerts + 1;

_pFaceData[ faceIndex + 4 ] = _numVerts + 2;

_pFaceData[ faceIndex + 5 ] = _numVerts + 3;


_numVerts += 4;

_numFaces += 2;

}

}



And finally, the renderer itself:





Code:
void RenderBuffer::Render()

{

glEnableClientState(GL_VERTEX_ARRAY); 

glEnableClientState(GL_COLOR_ARRAY);

glEnableClientState(GL_TEXTURE_COORD_ARRAY);


glVertexPointer( 2, GL_FLOAT, _stride, &_pVertData->vert );

glTexCoordPointer( 2, GL_FLOAT, _stride, &_pVertData->tex );

glColorPointer( 4, GL_FLOAT, _stride, &_pVertData->colour );


glDrawElements( GL_TRIANGLES, _numFaces * 3, GL_UNSIGNED_SHORT, _pFaceData );


Reset();

}


As I said, the actual render itself is pretty quick AFAICT - it's the addQuad and DrawImage calls that are very, very expensive for some reason.


D.
 
Code:
newX[ 0 ] = cosf( angle ) * ( quad.x[ 0 ] - x ) - sinf( angle ) * ( quad.y[ 0 ] - y ) + x;newY[ 0 ] = sinf( angle ) * ( quad.x[ 0 ] - x ) + cosf( angle ) * ( quad.y[ 0 ] - y ) + y; newX[ 1 ] = cosf( angle ) * ( quad.x[ 1 ] - x ) - sinf( angle ) * ( quad.y[ 0 ] - y ) + x;newY[ 1 ] = sinf( angle ) * ( quad.x[ 1 ] - x ) + cosf( angle ) * ( quad.y[ 0 ] - y ) + y; newX[ 2 ] = cosf( angle ) * ( quad.x[ 0 ] - x ) - sinf( angle ) * ( quad.y[ 1 ] - y ) + x;newY[ 2 ] = sinf( angle ) * ( quad.x[ 0 ] - x ) + cosf( angle ) * ( quad.y[ 1 ] - y ) + y;newX[ 3 ] = cosf( angle ) * ( quad.x[ 1 ] - x ) - sinf( angle ) * ( quad.y[ 1 ] - y ) + x;newY[ 3 ] = sinf( angle ) * ( quad.x[ 1 ] - x ) + cosf( angle ) * ( quad.y[ 1 ] - y ) + y;
Doing float maths is not a good idea. Doing trigo function on float is even worst. Consider using a sin/cos lookup table and factorize float manipulation as much as you can ;)


EDIT: also, I'm not seing any texture code there, not even a bind, color me surprised
 
Last edited by a moderator:
EDIT: also, I'm not seing any texture code there, not even a bind, color me surprised

Ah, that's elsewhere :)


The texture is 1024x1024, and is loaded here:



Code:
unsigned int GLInterface::LoadTexture( std::string filename )

{

sf::Image* img = new sf::Image;

if ( !img->LoadFromFile( filename ) ) { return 666; }


GLuint* spriteTexture = new GLuint;

glGenTextures(1, spriteTexture);


glBindTexture(GL_TEXTURE_2D, *spriteTexture);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img->GetWidth(), img->GetHeight(), 0, GL_RGBA, GL_UNSIGNED_BYTE, img->GetPixelsPtr() );


glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);


glEnable(GL_TEXTURE_2D);

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

glEnable(GL_BLEND);



TTexture* tex = new TTexture;

tex->name = spriteTexture;

tex->w = img->GetWidth();

tex->h = img->GetHeight();


_textures.push_back( tex );


GenerateTexRects( *tex );


delete img;

img = NULL;



return _textures.size() - 1;


}



and bound here, at render time:





Code:
void GLInterface::BindTexture( unsigned int textureNum )

{

if ( textureNum != _currentlyBoundTexture )

{

_currentlyBoundTexture = textureNum;

glBindTexture( GL_TEXTURE_2D, *_textures[ textureNum ]->name );


}

}



void GLInterface::RenderDynamicLayer( unsigned int layerNum )

{

assert( layerNum < _dynamicLayers.size() );


BindTexture( _dynamicLayers[ layerNum ]->textureNum );

_dynamicLayers[ layerNum ]->buffer->Render();


}


The actual u/v coords are pre-generated as texrects for each sprite.


D.
 
How about rewriting the entire game to match the Pandora needs? I bet this could lead into propper performance if done right. ^^" I even see this game running on a C64. :D
 
How about rewriting the entire game to match the Pandora needs? I bet this could lead into propper performance if done right. ^^" I even see this game running on a C64. :D
Well Dunny already have rewrite _large_ parts of the game. I wouldnt like to see him cross the line "it's a new game now" ;)
 
How about rewriting the entire game to match the Pandora needs? I bet this could lead into propper performance if done right. ^^&quot; I even see this game running on a C64. :D

The main problem is that this game is designed for the Pandora's hardware - we have very similar hardware to the iPhone 3GS which the game was originally written for!


D.
 
Code:
newX[ 0 ] = cosf( angle ) * ( quad.x[ 0 ] - x ) - sinf( angle ) * ( quad.y[ 0 ] - y ) + x;newY[ 0 ] = sinf( angle ) * ( quad.x[ 0 ] - x ) + cosf( angle ) * ( quad.y[ 0 ] - y ) + y; newX[ 1 ] = cosf( angle ) * ( quad.x[ 1 ] - x ) - sinf( angle ) * ( quad.y[ 0 ] - y ) + x;newY[ 1 ] = sinf( angle ) * ( quad.x[ 1 ] - x ) + cosf( angle ) * ( quad.y[ 0 ] - y ) + y; newX[ 2 ] = cosf( angle ) * ( quad.x[ 0 ] - x ) - sinf( angle ) * ( quad.y[ 1 ] - y ) + x;newY[ 2 ] = sinf( angle ) * ( quad.x[ 0 ] - x ) + cosf( angle ) * ( quad.y[ 1 ] - y ) + y;newX[ 3 ] = cosf( angle ) * ( quad.x[ 1 ] - x ) - sinf( angle ) * ( quad.y[ 1 ] - y ) + x;newY[ 3 ] = sinf( angle ) * ( quad.x[ 1 ] - x ) + cosf( angle ) * ( quad.y[ 1 ] - y ) + y;
Doing float maths is not a good idea. Doing trigo function on float is even worst. Consider using a sin/cos lookup table and factorize float manipulation as much as you can ;)

Lookup tables would be a great idea of course.... but about float - apparently float is actually faster than int, on iOS hardware. (with THUMB mode turned off). It's the best choice for games, from what I've been told.. :)


(Dunny - did you check to see if you're building with THUMB mode? and if so, try turning it off? it really slows things down on older iOS devices)


My DrawImage stuff is horrible inefficient :( I've no idea how the proper way to do this stuff is. I've been thinking about it, and figured out how to rewrite it to allow for things like only updating renderbuffers when something changes / only updating parts of buffers / etc... but it would require massive chunks of the game itself to be rewritten as well. One day..... !
 
Last edited by a moderator:
apparently float is actually faster than int, on iOS hardware. (with THUMB mode turned off).
That is pretty hard to believe. These 'older' devices have the same base architecture and the same set of extensions, just a few more MHz: Cortex-A8 core, VFPv3, NEON and no full FPU.


The VFP is said to be pretty useless leaving NEON as the only reasonable choice for doing lots of fp calculations. To reach a similar performance when doing such stuff as int you would need a pretty beefy FPU.


It is possible that not using Thumb might improve things slightly when using the VFP, I have read somewhere that using it causes the CPU to constantly leave and re-enter Thumb mode resulting in massive delays, but that won't turn the device into a fp monster.
 
Last edited by a moderator:
Ok.. I really don't know anything about all this stuff, just going by what I've seen people say.. haven't tested fixed vs floating point myself, as I haven't needed to. I do know that turning off THUMB mode gave a very noticeable speed increase on my 2G iPod Touch though :) (though I leave THUMB on for armv7 devices..)
 
Last edited by a moderator:
Back
Top