3D Puzzle Game "PZL" Released!

Back in, oh, last year I threw together a game for Ludum Dare 26 called “Puzzle Cube” that was built entirely on Ogre3D.   Shortly after, I ported that game to iOS and showed it to a friend who really encouraged me to develop it into a full-on mobile game with levels, scores, and everything. Since then I’ve made 30 code commits and added a bunch of features and levels and released it on the iTunes Store  under the official name PZL.  Dayum.

“IT’S LIKE SOME KINDA FRIKIN MAZE”

Well actually, in PZL you control a glowing blue orb that starts at the base of the puzzle tower.  Each level of the tower can be rotated so you can move the blue orb up till you reach the end of the level – and eventually the top of the tower.  Along the way you’ll discover tunnels through the puzzle, prizes, and monsters to evade.

The Journey.

So how did a game that started out looking like this:

Ludum Dare 26 – PuzzleCube

Get to this?

First of all, porting to an iOS mobile device when you start out with a Windows build isn’t a walk in the park.  In Windows your render loop is probably some endless while loop or frame listener.  On iOS you have an Objective C app that sets up a CADisplayLink callback on a frame interval.  The callback then manually instructs Ogre to renderOneFrame.  Then there’s the matter of getting input from the keyboard, touch input, outputting audio, not to mention any kind of networking or saving to disk you might need – ouch!
Let’s talk bout some of those –

Ogre3D

I used the OgreDemoApp/OgreFramework for iOS as a base so I had to refactor my initialization code so it worked with the OgreFramework.  I had to pass down some things like the scene manager, camera and window handle to my main game class.  I also decided to make my Game class a singleton mainly referenced through the OgreFramework so I could easily pass down frame render events and input events from OIS, but I probably could have architected it all differently and merged my main game class with the OgreDemoApp class.   The downside of merging would have been that I would have a lot more code in that merged class that wouldn’t get used on say the Windows version.
Also, I had to remove all the OgreBites tray code and the majority of the camera manipulation so my game could control it instead.

Audio

Things got a bit hairy when I needed to expose some things to C++ code that were only available in Objective C, namely audio, keyboard input and session data (stored in NSUserDefaults).
I chose to use the SimpleAudioEngine for audio which comes from CocosDenshion (Cocos2D also uses this).  It’s a really simple audio player that easily handles background and event audio.  So how do you access an Objective C class from inside game code which is written in C++?
What I did was create a GameAudio Objective C class:

@interface GameAudio : NSObject
@end

The implementation file for GameAudio (named GameAudio.mm) has my functions that call the SimpleAudioEngine class like so:

@implementation GameAudio
void playBackgroundMusic(std::string filename)
{
    [SimpleAudioEngine sharedEngine].backgroundMusicVolume = 0.25;
    [[SimpleAudioEngine sharedEngine] playBackgroundMusic:[NSString stringWithCString:filename.c_str() encoding:[NSString defaultCStringEncoding]]];
}

void stopBackgroundMusic()
{
    [[SimpleAudioEngine sharedEngine] stopBackgroundMusic];
}

unsigned int playEffect(std::string filename)
{
    return [[SimpleAudioEngine sharedEngine] playEffect:[NSString stringWithCString:filename.c_str() encoding:[NSString defaultCStringEncoding]]];
}

unsigned int playEffect(std::string filename,float pitch, float pan, float gain)
{
    return [[SimpleAudioEngine sharedEngine] playEffect:[NSString stringWithCString:filename.c_str() encoding:[NSString defaultCStringEncoding]] pitch:pitch pan:pan gain:gain];
}
@end

To call those GameAudio functions from C++ I provide another header file called GameAudioInterface.h with headers for the functions in GameAudio.mm


void playBackgroundMusic(std::string filename);
void stopBackgroundMusic();
unsigned int playEffect(std::string filename);
unsigned int playEffect(std::string filename,float pitch, float pan, float gain);

Keyboard Input

WARNING HAX.  I created a GameKeyboard Objective C class with an invisible UITextField added to the main UIWindow.

@interface GameKeyboard : NSObject

@property (nonatomic, strong) UITextField *textField;
    
+ (GameKeyboard *)sharedInstance;

@end

Inside the implementation file I provided functions to show/hide the keyboard and called functions in my main game code when a key was pressed.  Because the game code used OIS I had to map every character to the OIS equivalent. YUCK.

using namespace std;

map  createKeyMap() {
    map m;
    m['a'] = OIS::KC_A;
    m['b'] = OIS::KC_B;
    m['c'] = OIS::KC_C;
    m['d'] = OIS::KC_D;
    m['e'] = OIS::KC_E;
    m['f'] = OIS::KC_F;
    m['g'] = OIS::KC_G;
    m['h'] = OIS::KC_H;
    m['i'] = OIS::KC_I;
    m['j'] = OIS::KC_J;
    m['k'] = OIS::KC_K;
    m['l'] = OIS::KC_L;
    m['m'] = OIS::KC_M;
    m['n'] = OIS::KC_N;
    m['o'] = OIS::KC_O;
    m['p'] = OIS::KC_P;
    m['q'] = OIS::KC_Q;
    m['r'] = OIS::KC_R;
    m['s'] = OIS::KC_S;
    m['t'] = OIS::KC_T;
    m['u'] = OIS::KC_U;
    m['v'] = OIS::KC_V;
    m['w'] = OIS::KC_W;
    m['x'] = OIS::KC_X;
    m['y'] = OIS::KC_Y;
    m['z'] = OIS::KC_Z;
    m['0'] = OIS::KC_0;
    m['1'] = OIS::KC_1;
    m['2'] = OIS::KC_2;
    m['3'] = OIS::KC_3;
    m['4'] = OIS::KC_4;
    m['5'] = OIS::KC_5;
    m['6'] = OIS::KC_6;
    m['7'] = OIS::KC_7;
    m['8'] = OIS::KC_8;
    m['9'] = OIS::KC_9;
    m[' '] = OIS::KC_SPACE;
    return m;
}

map  keymap = createKeyMap();

@implementation GameKeyboard

    @synthesize textField;

+ (GameKeyboard *)sharedInstance {
    static GameKeyboard* _sharedClient = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedClient = [[GameKeyboard alloc] init];
    });
    
    return _sharedClient;
}

- (id)init {
    self = [super init];
    
    if(self) {
        self.textField = [[UITextField alloc] init];
        self.textField.delegate = self;
        self.textField.hidden = YES;
        self.textField.autocorrectionType = UITextAutocorrectionTypeNo;
        self.textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
        self.textField.keyboardAppearance = UIKeyboardAppearanceAlert;
        self.textField.returnKeyType = UIReturnKeyDone;
        
        NSArray *windows = [UIApplication sharedApplication].windows;
        UIView *parentView = nil;
        UIWindow *window = nil;
        if(windows.count > 0) {
            window = [windows objectAtIndex:0];
            //keep the first subview
            if(window.subviews.count > 0) {
                parentView = [window.subviews objectAtIndex:0];
                [parentView addSubview:self.textField];
            }
        }
        
    }
    
    return self;
}

void setKeyboardVisible(bool visible, const std::string& text)
{
    if(visible) {
        [[GameKeyboard sharedInstance].textField becomeFirstResponder];
    }
    else {
        [[GameKeyboard sharedInstance].textField resignFirstResponder];
    }
    [GameKeyboard sharedInstance].textField.text = [NSString stringWithUTF8String:text.c_str()];
}

void setKeyboardVisible(bool visible) {
    setKeyboardVisible(visible, "");
}

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    if(string.length == 0) {
        OIS::KeyEvent evt(NULL,OIS::KC_DELETE,0);
        PuzzleCube::getSingleton().keyReleased(evt);
    }
    else {
        // @TODO inject stuff here
        char c= [string characterAtIndex:0];
        if(keymap.find(c) != keymap.end()) {
            OIS::KeyEvent evt(NULL,keymap[c],c);
            PuzzleCube::getSingleton().keyReleased(evt);
        }
    }
    return YES;
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    setKeyboardVisible(false);
    OIS::KeyEvent evt(NULL,OIS::KC_RETURN,0);
    PuzzleCube::getSingleton().keyReleased(evt);
    return YES;
}
    
@end

Lastly, I exposed the keyboard show/hide functions in a header included in the C++ game code called

void setKeyboardVisible(bool visible);
void setKeyboardVisible(bool visible, const std::string& text);
Hopefully that code saves somebody some time.

Other  Objective-C Stuff n’ Thangs

OK besides audio and keyboard libraries I used NSUserDefaults to save some basic settings and the excellent Scoreoid API for saving player scores.  For those who are unaware, Scoreoid lets you save scores for free using their platform agnostic API so this means if I port the game to Android I can access the same score data in the future.
Lastly, I implemented the Chartboost SDK to serve ads.

Technology Used

Graphics Engine: Ogre3D
GUI: Gorilla
Input: OIS
Audio: SimpleAudioEngine (CocosDenshion)
Scores: Scoreoid
Animation: CppTweener
Network: AFNetworking
That pretty much about sums things up as far as the development goes.  I do plan on adding some more gameplay elements in the future if the game has enough of an audience to warrant updates.
You can download PZL via the iTunes App Store on your mobile device or click the link here:
https://itunes.apple.com/us/app/pzl/id812626888
Thanks and enjoy!

, , , , ,