Friday, July 13, 2012

Beware of automatically downscaled retina images


The iOS development environment facilitates image content loading for retina and non-retina devices with a clever naming scheme. If an iOS app is running on a retina device, and is instructed to load an image named foo.png, it will actually attempt to load the foo@2x.png file if it exists. By providing both foo.png and foo@2x.png images, both classes of devices are supported.

This convenience offered by UIKit goes one step further: if you do not provide the regular resolution version foo.png but only the retina version foo@2x.png then non-retina devices will load the high res version and automatically downscale a factor 2 so that it can be used. If you are tempted by just providing retina versions of your images, and skip the regular versions, like I was, you are selling your app short. It turns out that UIKit does a horrible job at downscaling, and the image will look considerably worse than a version that you pre-scaled yourself.

To illustrate the effect, see below how the original is downscaled by UIKit, and how it is downsampled by my authoring tool inkscape. A big difference, I would say.

The bottom line: if you care for your iPad2 and iPhone3GS Users, don't skimp on images. Provide both the foo.png and the foo@2x.png files.


The retina version of the graphic, viewed at 1:1 zoom.


The image automatically downscaled by UIKit from the retina version, viewed at 2:1 zoom.


The image, exported at non-retina resolution by inkscape, viewed at 2:1 zoom.

Monday, June 25, 2012

Bram's upcoming indie game

I've been working on my next indie game. It uses a voxel art style, and like the little crane that could, it features a top notch physics simulation. Here are some screenshots of the game. Note: the new project has not been named yet.


On the farm.


Visiting the queen.


At the picnic.

Monday, May 21, 2012

De Nederlandsche taal, het redden niet waard?

Zittend in mijn badkuip overpeinsde ik de betekenis van het woordje weer. Dit woordje heeft een verscheidenheid aan betekenissen, geillustreerd met de zin:

Wegens het stormachtige weer moest de brandweer vandaag weer uitrukken.

Dan doen de oosterburen het toch een stuk preciezer in de Duitse taal. Met mijn VWO Duits (en Google) maak ik er het volgende van:

Wegen des stürmischen Wetters musste die Feuerwehr heute wieder ausrücken.

Waarom is de Nederlandse taal zo slordig? Mijn vermoeden is dat deze termen niet altijd op één hoop werden gegooid, getuige het oud Nederlandsche woord onweder en de term wederom. Door de eeuwen heen zijn de termen gedegenereerd tot één enkel woord. Door al die jaren heen hebben de Nederlanders het niet zou nauw genomen met hun taal, dus waarom zouden we dat in de 21e eeuw wel moeten doen? Van mij mogen dus de colleges aan de Nederlandse universiteiten best in het Engels, Duits of zelfs Kantonees (toekomstgericht!) worden gegeven. Het redden van de taal hadden we 500 jaar eerder maar moeten doen, nu is het denk ik de redding niet waard.

Thursday, May 17, 2012

Building a brand

I had this printed at Make Vancouver, so I can build my brand. I will be wearing this during Apple's World Wide Developer Conference, so I can get some exposure for the fine game that is 'the little crane that could'. It is a full colour digital print. I was considering a silk screen print, but that requires reducing the number of colours. I am still toying with the idea of getting an embroidered jacket to go with it.

Tuesday, March 27, 2012

Making good progress on the World Editor

I am making good progress on the world editor for the little crane that could. I am creating a stand alone companion app for the game with which you can build your own levels. The levels are stored in iCloud, so that you can play them on any iOS device you own. You can also send your designs to friends via email. Here is a screen shot of the game menu that shows a user created level in the list.

The world editor and the game are both supporting the retina display of the new iPad. Man, oh man, that 2048x1536 resolution sure looks marvellous, and is ideally suited for level editing.

Tuesday, February 21, 2012

Skycrane

My game the little crane that could is still going strong, so I keep releasing updates for it. Lately, it has been very popular in Japan, not sure why. A lot of Japanese downloads, but the conversion rate in Japanese market is pretty low. Anyway, the next update will feature something special: a Skycrane.

Just like I did for the crane on wheels, I am taking the high road: a proper full blown physics simulation. This means that the forces of lift actually work on the rotor blades, where force leads to acceleration leads to velocity leads to movement. Also, the force that spins the main rotor causes the body of the helicopter to counter rotate, so I apply an anti-torque force with the tail rotor. Note that the tail rotor is not yet visualized in the image. Also missing are wheel struts and some sort of winch with hook that will enable the player to grab objects.

After implementing this elaborate and accurate simulation, I found out that controlling the helicopter was neigh impossible. It was simply too hard to steer due to oscillations and spiralling out of control. That is why I added two PID controllers that sit between the cyclic controller (joystick) and the control surfaces. They translate the player's intent into steering signals, 180 times per second. With PID control you can fix overshoots, oscillations and steady state errors by carefully tuning the proportional, integral and derivative constants. Now the Skycrane flies like a dream: it has nice, yet realistic handling characteristics.

Monday, January 2, 2012

Walk in the park

Because mommy had a day off today, we were able to make a little walk in the park with Annelies this morning. Annelies enjoyed it very much, and loved playing with doggies and getting attention from the women. Nobody can resist her when she puts on her best smile and start giggling. Annelies is excited about her grandparents visit at the end of this month. We all look forward to seeing them again.

Sunday, January 1, 2012

A 1000 soldiers

My explorations of Android Development are starting to find shape. Here is a screenshot from my tablet device showing a lot of the little buggers on the march. Now I have to find a good gameplay mechanic to make directing a small army fun for the player. In my younger years (early 90s) I used to play xbattle on the University's Sun workstations. That was a lot of fun, so it would be an excellent starting point. And I think that a touch interface is a much better fit for an xbattle game than a mouse interface is, so that should give me an edge. The performance is pretty good on the Acer Iconia, despite the modest Tegra2 specifications that don't even include Neon SIMD support.

Saturday, December 17, 2011

Android Explorations


Today I bought an Android powered tablet, the Acer Iconia 7" tablet. As my friends keep telling me, I should start doing Android development, so that I can port the little crane that could to Android devices. Apart from the horrible colours and contrast of the display, I am satisfied with the device so far. Graphics performance seems way behind what an iPad2 can do though, so graphics optimizations are going to be important. Here are some things I learned about Android programming so far:

  • You need to install an SDK and an NDK.
  • As of recently, you can do native development in C without writing a single line of Java code. To do this, see the NativeActivity sample from the NDK.
  • You can do development from the command line without IDE's.
  • Instead of make, you need to use a combination of ndk-build and ant.
  • GLESv1_CM is the OpenGL ES1 library to use. CM stands for COMMON and means that it uses floating point, not fixed point.
  • Doing 'ant debug' and 'ant installd' is often not enough, as I need to do a clean before building. Are the dependencies not set up properly in the sample projects?
  • The emulator cannot do OpenGL ES2, only ES1. When doing ES1 it is super slow, even on a modern mac.
  • The equivalent of GLX for Android is called EGL
  • Android NDK by default targets the ARMV6 devices, which means software floating point emulation. This is very slow.
  • To target ARMV7 with hardware floating point (VFP), add this to jni/Application.mk: APP_ABI := armeabi-v7a
  • To see verbose output of the build process, use ndk-build V=1
  • Loading assets from native code is a painful experience. So instead I convert images to C source code, and build as a shared object. Then I use dlopen() to get to the data.
  • To debug simply run ndk-gdb --start from the project dir.
  • You can set java compiler flags on the ant command line like this: ant "-Djava.compilerargs=-Xlint:unchecked -Xlint:deprecation" debug
  • You can make screenshots with the ddms utility. Select the device and press ⌘S to grab the screen.
  • To incorporate a third party framework, all you have to do is copy the .jar file into your projects ./lib/ directory, and the build system will automagically find and use it.
  • If you want to install certain resource types without compression, then you need to edit your SDK. Change the tools/ant/build.xml file by adding a < nocompress extension="foo" /> to the aapt section.
  • Don't get confused by -msoft-float and -mhard-float, they do not toggle the hardware floating point unit. Instead they influence the convention for passing function arguments. So using -mhard-float will only get a modest speed bump, at the cost of decreased ABI compatibility.
  • in jni/Application.mk you need to keep APP_PLATFORM set to android-20 or lower. At android-21, downward compatibility breaks, and on a pre-android L device, your app will crash with a 'failed: dlopen failed: cannot locate symbol "rand" referenced by...' error.
I am using The Android NDK Beginner's Guide as reference, as well as a guide by IIvan Vučica. That last one is pretty ambitious: it sets up the development environment so that you can do Objective C for Android. However, I decided to keep it simple, and do my project in C.
There is an excellent GDC presentation from Lars Bishop of nVidia on android development.
For future reference, you need to set up some environment variables. This is what I use in my .bashrc file:
export ANDROID_NDK_ROOT=$HOME/android-ndk-r9d
export ANDROID_SDK_ROOT=$HOME/android-sdk-linux
export NDK_MODULE_PATH=$HOME/apps:$HOME/src
PATH=$PATH:$ANDROID_NDK_ROOT:$ANDROID_SDK_ROOT

Friday, December 16, 2011

First Art

Annelies made her first art today at day care. Well art, not sure if you can call it art, but she did paint a picture. Some bold use of colour! The teacher misspelled her name though.

Thursday, December 15, 2011

Nieuwe Nederlanders / New Dutchies

En met deze handdruk is Amy een Nederlandse geworden. Haar man is Nederlands, haar dochter is Nederlands, maar dankzij Amy's Nederlandse moeder is Amy nu genaturaliseerd door de deputy consul in Vancouver.

And with this handshake, Amy became a Dutch citizen. Her husband is Dutch, her daughter is dutch, but thanks to Amy's Dutch mother, Amy could be naturalized by the deputy consul in Vancouver.

Tuesday, December 6, 2011

Don't bother buying Infinity Blade II

So, after all that hype, I decided it was time to find out what Infinity Blade II was all about. I've heard things attributed to it as 'best looking game on iOS' and such, so as a game developer myself I wanted to see it. So I pay the $6.99, got my fancy noise cancelling headphones for the optimal experience, and then..... NOTHING! The game crashes during the title credits. Tried a few times, a 100% crash. I stopped ALL backgrounded apps on my iPad2, and tried again... Crash.

I've noticed this weird trend where the bigger the studio/publisher, the more crashes I see. Why do these big studios get away with publishing crap, and still rank among the top grossing titles? I noticed it with EA's SimCity Deluxe for iPad. The names don't get any bigger than that: EA, Sim City. Yet, they are unable to create a stable experience without crashing. My advice to the consumers here: forget about that big studio crap, and buy indie games. You will typically get email support from the author directly, and there is someone who actually cares about you. Don't let EA and Chair Entertainment get away with this: just stop buying from them.

And you don't have to take my word for it, there are plenty of crash reports from users. In case someone at Chair entertainment decides to do something about it, here is the log of a run where there is not a single other app running in the background:


Dec 6 18:35:18 unknown kernel[0] : launchd[11064] Builtin profile: container (sandbox)
Dec 6 18:35:18 unknown kernel[0] : launchd[11064] Container: /private/var/mobile/Applications/6CD18407-B4A9-4692-AA34-D07EBAF21381 [69] (sandbox)
Dec 6 18:35:18 unknown SwordGame[11064] : Installing signal handler
Dec 6 18:35:19 unknown SwordGame[11064] : Unknown movie state 0
Dec 6 18:35:19 unknown kernel[0] : virtual void AppleCLCD::mieEnable(bool), enable: 1
Dec 6 18:35:20 unknown SwordGame[11064] : This device is [iPad2,1], enum value 5
Dec 6 18:35:20 unknown sandboxd[11065] : SwordGame(11064) deny file-write-data /private/var/mobile/Library/Mobile Documents/8XJ6WJ8Z84~com~chairentertainment~IB2
Dec 6 18:35:21 unknown kernel[0] : launchd[11067] Builtin profile: gamed (sandbox)
Dec 6 18:35:22 unknown gamed[11067] : 18:35:22.436090 com.apple.AVConference: GKSConnSettings: set server: {
"gk-cdx" = "17.173.254.221:4398";
"gk-commnat-cohort" = "17.173.254.223:16386";
"gk-commnat-main0" = "17.173.254.222:16384";
"gk-commnat-main1" = "17.173.254.222:16385";
}
Dec 6 18:35:23 unknown SwordGame[11064] : 18:35:23.312345 com.apple.AVConference: GKSConnSettings: set server: {
"gk-cdx" = "17.173.254.221:4398";
"gk-commnat-cohort" = "17.173.254.223:16386";
"gk-commnat-main0" = "17.173.254.222:16384";
"gk-commnat-main1" = "17.173.254.222:16385";
}
Dec 6 18:35:23 unknown kernel[0] : virtual void AppleCLCD::mieEnable(bool), enable: 0
Dec 6 18:35:23 unknown SwordGame[11064] : Initialize!
Dec 6 18:35:23 unknown SwordGame[11064] : FBDelegate: !
Dec 6 18:35:23 unknown SwordGame[11064] : SetAppID to 152777738124418 / (null) / (null)!
Dec 6 18:35:23 unknown SwordGame[11064] : FBDelegate.FB: !
Dec 6 18:35:24 unknown SwordGame[11064] : GLView: 1357870
Dec 6 18:35:24 unknown SwordGame[11064] : GLView: (EAGLView: 0x1357870; frame = (0 0; 1024 768); layer = )
Dec 6 18:35:27 unknown SwordGame[11064] : Requesting product id com.chair.IB2.goldbag.25000
Dec 6 18:35:27 unknown SwordGame[11064] : Requesting product id com.chair.IB2.goldbag.150000
Dec 6 18:35:27 unknown SwordGame[11064] : Requesting product id com.chair.IB2.goldbag.750000
Dec 6 18:35:27 unknown SwordGame[11064] : Requesting product id com.chair.IB2.goldbag.2500000
Dec 6 18:35:27 unknown wifid[27] : WiFi:[344918127.366272]: Client itunesstored is background application
Dec 6 18:35:27 unknown librariand[11053] : item update observer error: Connection invalid
Dec 6 18:35:28 unknown SwordGame[11064] : Reading int value for key CurrentSlot369d86c1e1a21755d7f29b7aa7ac2065436e116d
Dec 6 18:35:30 unknown MobileMail[11062] : Received memory warning.
Dec 6 18:35:30 unknown SwordGame[11064] : Received memory warning.
Dec 6 18:35:31 unknown SpringBoard[15] : Received memory warning.
Dec 6 18:35:31 unknown syncdefaultsd[11058] : received memory warning
Dec 6 18:35:32 unknown SwordGame[11064] : Reading int value for key LocalSaves_dvb54c8b744f649484cfcae3478da0ec06
Dec 6 18:35:36 unknown com.apple.launchd[1] : (com.apple.gamed) Exited: Killed: 9
Dec 6 18:35:36 unknown com.apple.launchd[1] : (UIKitApplication:com.apple.mobilemail[0x45a2]) Exited: Killed: 9
Dec 6 18:35:36 unknown com.apple.launchd[1] : (UIKitApplication:com.apple.mobilephone[0x6ebe]) Exited: Killed: 9
Dec 6 18:35:36 unknown com.apple.launchd[1] : (UIKitApplication:com.chairentertainment.IB2[0x11fc]) Exited: Killed: 9
Dec 6 18:35:36 unknown UserEventAgent[12] : jetsam: kernel termination snapshot being created
Dec 6 18:35:36 unknown SpringBoard[15] : Application 'Mail' exited abnormally with signal 9: Killed: 9
Dec 6 18:35:36 unknown mediaserverd[43] : 18:35:36.366 AudioSessionSetClientPlayState(11064): cannot get ClientInfo
Dec 6 18:35:36 unknown mediaserverd[43] : 18:35:36.532 AudioQueue: Error 'ini?' from AudioSessionSetClientPlayState(11064)
Dec 6 18:35:36 unknown kernel[0] : launchd[11070] Builtin profile: MobileMail (sandbox)
Dec 6 18:35:36 unknown SpringBoard[15] : Application 'FaceTime' exited abnormally with signal 9: Killed: 9
Dec 6 18:35:36 unknown SpringBoard[15] : Application 'InfinityBlade2' exited abnormally with signal 9: Killed: 9
Dec 6 18:35:37 unknown ReportCrash[11069] : Saved crashreport to /Library/Logs/CrashReporter/LowMemory-2011-12-06-183537.plist using uid: 0 gid: 0, synthetic_euid: 0 egid: 0
Dec 6 18:35:38 unknown librariand[11053] : client connection is invalid: Connection invalid

Hover City at night

Hover Biker version 1.0 was just uploaded to Apple for review. Hopefully they get to it before dec 22, at which time they will stop processing submissions for a week. It would be really nice to have my game available on the app store before Christmas. I've put in a night-time level in the game, which brings the number of levels to 6 free levels, 7 premium levels and 1 tutorial level. I keep wondering how well it will do on the app store. I am hoping to replicate Little Crane's success, but as a worst case scenario, I can't see it do any worse than Panzer Class, which does not sell spectacularly, but still brings in money. But the good news is that every iPad sold by Apple this month is a new potential customer.

Wednesday, November 30, 2011

Gameplay Video

Here is a video that shows the gameplay in my new game Hover Biker. I shot it with a webcam pointed at my monitor fed with an HDMI signal of the iPad2 in mirroring mode. So the quality is not that great. I would have liked to record it with a BlackMagic Hyperdeck Shuttle, but BlackMagic is not responding to inquiries about iPad2 compatibility of their product. Too bad, iOS devs could be a large customer base for them. Feel free to critique, suggest or discuss it, over at the Hover Biker thread at the Touch Arcade forums.

The music is again by the highly talented Johan Stolk who composed the score, and played all the instruments as well. Please note how I timed the first long draw on his guitar with the moment that the robot pushes the right throttle lever. I can't stop watching that.

Keep an eye out on App Battleground where there may appear a review of my game, which is now available at the app store.


Thursday, November 24, 2011

Teaser shots from my new game

Hot off the press... some teaser shots of my new game that is currently under development: Hover Biker. In many ways it will be a better game than the little crane that could. Better graphics, more action, and more mass appeal. What has not changed is a first-rate unsurpassed physics simulation at the root of the game.

This time around, I purchased professional 3D artwork for the city environment. Unlike my own art (which we call programmer art in my industry) it has textures applied. The game will feature a very rich city environment that is completely accessible for the player. This is the opposite of a 'game on rails', if your hover bike can reach it, you are free to visit every nook and cranny of the city.

I will release the game with plenty of challenging levels, but just high speed hovering and pulling stunts with the hover bike is a treat in itself. The game should be in the app store for iPad and iPhone this Christmas. I have a feeling that this game could do very well, and is capable of making some big waves on the app store. There is potential to equal or surpass little crane's success.

Wednesday, October 26, 2011

Loading PVRTC textures without blocking iOS User Interface

While working on my new iOS game, I decided to add one of those nice view transitions when you start a new level. At first I was puzzled by the lack of a nice animation, but then it dawned on me why: iOS will only remain smooth, and show its fancy animations as long as you do not block the main thread. When starting a level, I would first load large textures on the main thread. So how to avoid this?

iOS makes it easy to offload heavy lifting to other threads by using Grand Central Dispatch and its Objective-C construct for code blocks. The trick is to decide what to offload. At first I naively did the whole texture creation on another thread. The downside of this is that you now have two threads competing for the same OpenGL context. A way around would be mutex, but there is a more elegant and simple approach to it that does not involve locking the context or adding a mutex.

The slow part of texture creation is not the uploading with glTexImage() neither the creation of the texture name with glGenTextures(). It is the file loading of the image data, and the decoding of the PVRTC that gobbles up cycles. So only the loading and decoding has to be moved to another thread. All the OpenGL calls can remain on the main thread, and no context locking is required.

I expect that most iOS developers are using the Apple-provided class PVRTexture.h/PVRTexture.m which can be found at the developer.apple.com site. Adapting this class to offload the texture load/decode on another execution thread is simple. Just follow this recipe:

First add a new member variable to PVRTexture.h


dispatch_group_t dispatchGroup;

Then, add a static var and a class method to PVRTexture.m


static dispatch_queue_t dispatchQueue;

+(void)initialize
{
dispatchQueue = dispatch_queue_create( "textureLoadingQueue", nil );
}

Next, replace this section in createGLTexture


if ([_imageData count] > 0)
{
if (_name != 0)
glDeleteTextures(1, &_name);

glGenTextures(1, &_name);
glBindTexture(GL_TEXTURE_2D, _name);
}

with just a single line


glBindTexture(GL_TEXTURE_2D, _name);

last, replace the implementation of initWithContentsOfFile with


- (id)initWithContentsOfFile:(NSString *)path
{
self = [super init];

glGenTextures( 1, &_name );

dispatchGroup = dispatch_group_create();

dispatch_group_async( dispatchGroup, dispatchQueue, ^{
//NSLog( @"Loading texture data from file" );
NSData *data = [NSData dataWithContentsOfFile:path];

_imageData = [[NSMutableArray alloc] initWithCapacity:12];

_width = _height = 0;
_internalFormat = GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
_hasAlpha = FALSE;

assert( data );
const BOOL unpacked = [ self unpackPVRData:data ];
assert( unpacked );
});

dispatch_group_notify( dispatchGroup, dispatch_get_main_queue(), ^{
//NSLog( @"Creating GL texture for name %d", _name );
const BOOL created = [ self createGLTexture ];
assert( created );
});

dispatch_release( dispatchGroup );
dispatchGroup = 0;

return self;
}

Some notes: I added asserts to catch missing texture data and such. If you want more graceful failures, like the original had, you may want to adapt that. Also: with this change you can do some really neat stuff, like starting your game before the textures are loaded. OpenGL will happily draw the scene if the texture data is not yet uploaded to the GPU. The world will incrementally be populated with textures, before the player's eyes. Less Waiting, More Playing: how is that not a win?

I used the same tactic for the creation of OpenDE triangle meshes used for my physics simulation. I simply keep the sim delta-time at 0.0 as long as the triangle mesh has not been built yet: let the player absorb the environment while the game is still doing its initializations.