Shoot to Clean – Behind the Scenes

As described in the previous post, we had a project featuring an outdoor game in Eilat Avenue, for Wrigley Israel. In this post I will elaborate more on the technical aspects of the game.

The Set

The game was displayed over a 8m X 5m LED screen. It was an excellent out-door screen, colorful and sharp even from a distance of hundreds of meters.

All the game components (servers and devices) were connected through a local WIFI ethernet which supported up to 700 devices simultaneously, and worked perfectly. The network consisted of 10 access points (WIFI antennas) spread all over the area, all connected to one main router.

The Game

The game itself is an Adobe AIR (Flash) app. It runs a SocketServer object which handles real time communications with all the player devices which are currently participating in the game  (not those who wait in the queue).

Each player controls an avatar that looks something between a tank and a spaceship. The avatars differ from each other by color and number. The avatar’s color and number appear on the user’s device screen, and if the user got confused and lost track of their avatar, they can click on a special “Show Me” button in their device to make their avatar blink on the big screen.

The enemies are “food creatures” (from the Orbit campaign), based on foods and beverages which are considered non healthy for your teeth: cookies, sushi, chocolate, juice etc. The bullets are Orbit chewing gums. Special power-ups come in the form of Orbit packs that are thrown from airplanes, and include extra-powerful shots, protection bubble, health (measured in pH units), score bonuses and so on. Three background themes (forest, desert, snow) mark different game scenes; a scene ends after winning a Boss level, when all the players fight a single big creature, or combine their powers to explode a nuclear bomb.

Unlike a standard arcade game, the levels difficulty does not increase over time. The game is ongoing forever, and players can join whenever there’s a free spot (even in the middle of a “level”). However, the game level does increase gradually for each player individually – so it’s hard to stay in the game for more than a few minutes.

To create a challenging game experience, we worked hard on balancing levels and attack waves. We created more than 20 basic scenarios for attack waves, each having tons of randomized parameters. The result was a huge set of possible attack scenarios, which were randomly combined into “levels” (3-4 attack waves in each level). This enabled us to introduce the challenge of “finishing a level” into the game, and have difficulty changes in the plot; for example: a level may start with a squad of doughnut creatures firing small doughnut-bullets and led by a coffee-cup-commander, followed by a massive air force attack (airplanes, parachutes, bombs and power-ups), followed by a single boss creature in the form of a big evil strawberry.

One of our main goals was to entertain not only the players of the game, but also the rest of the audience which was merely watching. The level-building interface I’ve just described enabled us to do exactly that – within a week we created many scenes of completely different atmospheres, which created a feel of “a plot” in the game. We named one of our favorite scenes “Planet of the Bananas”, where dozens of small bananas raid the arena in a choreography that looks like a scene from “Planet of the Apes”.

The game uses a 2.5D mechanism, implemented over the standard 2D flash engine. Our in-house developed engine simulates 3D, by resizing and reordering objects on a virtual z- axis. The result is a visual feel of the creatures running down-the-hill and moving towards the crowd – a request that came from the customer, and was also needed to fit the 3D graphic concept of the Orbit campaign.

An external admin-tool was also provided, to change game parameters on the go – for example, limit the number of simultaneous users, make the game harder/easier, display admin notifications on the screen, etc.

The Queue Manager

The queue was managed by a web-server, written in Java. It constantly updated all the user devices with their queue position, and notified them when their turn arrived and they should start playing (by connecting directly to the game application).

The server also provided a web-admin application, which enables an administrator to check the queue status, kick players with inappropriate nicknames, and promote users in the queue (i.e. when a well-known blogger comes to check out the game – make sure that she doesn’t have to wait).

The Clients

Two separate client applications were implemented – one for Android devices, and another for iOS.

Both applications perform similar tasks – they first connect to the queue manager, using HTTP, and manage the user registration process. They then wait for a “go” indication from the queue manager (when my turn to play arrives), and connect directly to the game app.

During the game, they use the device sensors (accelerometer and screen-taps) to send instructions to the game app (move, fire, “Show Me”). They also display status-updates received from the game app (current score & life, hit events, bonus events, etc.).

On game over – they reconnect to the queue manager, to obtain and display statistics about the game, and optionally – register in the queue for another round.

Android Client App

This is a native Android app, written in Java and published to the market. To install it, users had to navigate to the market, or download the application from a direct URL in the local network.

The communication with the game used a proprietary protocol over standard (binary) sockets. The app played sound effects on several game events, and even vibrated on hit.

Generally speaking – our Android app received the best reviews from users – it worked great, had no noticeable lags, and the sound/vibration effects made a great impact.

iOS Client App

The iOS devices didn’t get a real app (such that you download from the App Store), but rather used an HTML5 web application. All users needed to do was to follow a link in the browser, and start playing. This enabled us to avoid Apple’s bureaucracy and the cumbersome processes of app registration, approval and version update: in such a tight-schedule project, we just couldn’t afford them.

To implement all the required features in an HTML5 client, we had to jump through hoops and dig into some real tough issues:

1) Web Sockets. HTML5 does not provide standard TCP sockets. Instead, we used the (relatively new) Web Sockets interface, which works quite differently. For example, these sockets do not support binary data (only textual data). Also, there’s no way to flush the socket (send the data immediately, without OS buffering). This resulted in much greater lags than in the Android application. And on top of it all – some messages appeared to just “get lost in the way” every once in a while.

We needed to apply tons of tricks to optimize the communication and reduce lags (for example, to find the exact best send-rate of control messages, which gives the smallest lag but still doesn’t burden the CPU and network).

2) Fullscreen and Rotation issues. Our client app was designed for fullscreen landscape view. Unfortunately, this is simply not supported in Safari, which cannot unlock the auto-rotation feature. We had to build two versions of the screens, both for landscape and portrait mode (each having different sizes and positioning). Making the view look the same in all the 4 possible orientation states was quite a headache of HTML/CSS/JS.

Furthermore – this specific application, by definition, requires the user to continuously tilt the device (in order to move the avatar). This created constant jumps in display, whenever the auto-rotation point was reached. The only solution we found was to recommended to lock the auto-rotation feature of your device while playing.

3) Sound. The sound interface of HTML5 worked terribly on Safari. We encountered huge performance lags (sometimes a few seconds long), and totally unexpected behaviors when trying to play several sounds simultaneously. Eventually, we just gave up and dropped the whole sound feature out of the iOS client.

4) Vibrate. HTML5 still does not provide access to many device features, like vibrate. So here also, we had to settle on a simpler app, with no fancy vibrate feature.

 

All in all – the iOS application got reasonable reviews, and worked relatively fine, but we did encounter some lags, and a few other infrequent problems, even in the release version. And of course – the lack of sounds and vibration made it much duller than its Android equivalent.

 

  

 

LinkedInShare
Posted in android, Flash/Flex/AIR, games, HTML5, mobile app development | 1 Comment

Introducing: Shoot to Clean Eilat 2011

  

Last month (October 2011) we produced a unique fun project: a multi-player street game, displayed over a huge LED screen, where anyone can play through their personal smartphone or tablet. More than 1000 players participated in the game, which took place in Eilat‘s beach avenue. Here’s a taste of the atmosphere in the game arena.

The project was a part of a campaign for Wrigley‘s chewing gum brand – Orbit, through the Gitam/BBDO advertising agency.

The game was an adaptation of “Space Invaders”, by the content world of the Orbit brand:

Each player controls a small tank-like avatar using their personal device as a control-pod (android/iOS). Tilting the device left/right/forward/backward moves the tank, and tapping the touch-screen shoots.
The goal of the game is to shoot food creatures using chewing gum bullets, and score as many points as you can. The creatures shoot back, airplanes drop bombs and power-ups, suicide bombers infiltrate the arena, and each hit increases the player’s pH (life meter). The player dies when the pH level reaches the maximum level.

Up to 10 players can play simultaneously, each identified by a unique color and number. To play the game, users should register to a virtual queue (online, through their smartphones); whenever a player leaves the game (or gets killed) – the next player in the queue automatically joins. An average game session lasted around 4 minutes, and the average wait-time in the queue was around 10 minutes.

The event lasted 3 days from 17:00 to 23:00. A total of 2800 games were played by more than 1000 unique players, using various devices such as Android phones & tablets, iPhone, iPod Touch and iPad. The game was displayed over a 8m by 5m LED screen, which made it visible from miles. It used a private WIFI network, TCP/IP and WebSockets for communication between the devices and the game servers.

Here are some captures:

We had lots of fun, just sitting there and watching so many people enjoying the game. Some kids dragged their parents to play for 3 days in a row…

For more technical details, read the next post.

LinkedInShare
Posted in android, Flash/Flex/AIR, games, mobile app development | Leave a comment

Flex for PlayBook: Bridging over QNX classes

While working on a Flex application for the RIM (BlackBerry) PlayBook tablet, I encountered a problem: some QNX classes are not supported on desktop, crashing my app.

For those of you who are not strongly familiar with PlayBook and QNX: PlayBook uses an operating system called QNX. This operating system is based on Adobe AIR – which means that everything on the device actually runs in a sort of a Flash Player. You can develop your standard Flex application as usual; however, QNX provides some extra OS classes (e.g., qnx.system.AudioManager for controlling audio volume on the device). There are even special QNX UI-components (like qnx.ui.buttons.Button), which you can use if you want to get the standard look & feel of the device.

This is all very nice, only that some QNX classes actually need the physical device – like qnx.system.AudioManager (audio I/O), qnx.input.IMFConnection (virtual keyboard) and qnx.system.Device (device info) – these work beautifully on the device itself, but when I run the application on desktop – I get an error like this:

VerifyError: Error #1014: Class qnx.pps::PPS could not be found.

Personally, I prefer debugging my applications on desktop, and only then – continue the debug process on the device itself. Bummer. Even a ‘try/catch’ (or a simple ‘if’) won’t help here – such a “missing class” error occurs  when the classes are initialized (usually on application startup), in the core of the Flash Player – therefore, it cannot be caught in my code.

To solve the problem I wrote a small package of simple bridge classes. I moved all the problematic QNX calls into a separate class which I use on PlayBook only. On desktop, I use a different implementation for the same features, and thus – the error is avoided:

IDeviceBridge: An interface which includes all the problematic (device-specific) features. These features will be implemented differently on desktop and PlayBook
PlayBookDevice: Implements IDeviceBridge with QNX classes
DesktopDevice: Implements IDeviceBridge with standard Flex classes (this does work on desktop)
DeviceBridge: A simple class which only chooses which of the above two classes should be used – PlayBookDevice or DesktopDevice.

The result looks like this. From anywhere in my code I can now call functions which are specific to the actual device I’m currently using:

if (DeviceBridge.device.deviceId == "42")
{
	trace("This is my device, and it works both on PlayBook and desktop!");
}

Here’s a simple version of the code (in reality it contains many more device-specific features. You can obviously change/add stuff to the classes to include more features):

///////////////////////////////////////////////
// IDeviceBridge is an interface, containing all the functions which are device-specific.
// It extends IEventDispatcher, to enable firing events from within the bridge.
//////////////////////////////////////////////

import flash.events.IEventDispatcher;

public interface IDeviceBridge extends IEventDispatcher
{
	function hideKeyboard():void;
	function get audioOutputLevel():Number;
	function set audioOutputLevel(value:Number):void;
	function get deviceId():String;
}
//////////////////////////////////////////////
// PlayBookDevice implements IDeviceBridge on a PlayBook device,
// using the QNX classes
//////////////////////////////////////////////
import flash.events.Event;
import flash.events.EventDispatcher;

import qnx.events.AudioManagerEvent;
import qnx.input.IMFConnection;
import qnx.system.AudioManager;
import qnx.system.Device;

public class PlayBookDevice extends EventDispatcher implements IDeviceBridge
{
	public function PlayBookDevice()
	{
                // Listen to changes in device audio level and notify user
		AudioManager.audioManager.addEventListener(AudioManagerEvent.OUTPUT_LEVEL_CHANGED,
                                                           onAudioManagerChange);
	}

	private function onAudioManagerChange(event:AudioManagerEvent):void
	{
		dispatchEvent(new Event(DeviceBridge.AUDIO_LEVEL_CHANGE));
	}

	public function hideKeyboard():void
	{
		IMFConnection.imfConnection.hideInput();
	}

	public function get audioOutputLevel():Number
	{
		return AudioManager.audioManager.getOutputLevel();
	}

	public function set audioOutputLevel(value:Number):void
	{
		AudioManager.audioManager.setOutputLevel(value);
	}

	public function get deviceId():String
	{
		return Device.device.pin; // Uniquely identifies the device
	}
}
/////////////////////////////////////
// DesktopDevice implements IDeviceBridge on desktop, without using QNX classes
/////////////////////////////////////
import flash.events.Event;
import flash.events.EventDispatcher;

public class DesktopDevice extends EventDispatcher implements IDeviceBridge
{
	private var _audioLevel:Number = 1; // No audio level access on desktop.
                                            // This only simulates audio level

	public function hideKeyboard():void
	{
                // No such thing on desktop
	}

	public function get audioOutputLevel():Number
	{
		return _audioLevel;
	}

	public function set audioOutputLevel(value:Number):void
	{
		_audioLevel = value;
		dispatchEvent(new Event(DeviceBridge.AUDIO_LEVEL_CHANGE));
	}
	public function get deviceId():String
	{
                // Randomize some ID
		return "Desktop" + Math.floor(Math.random() * 1000);
	}
}

/////////////////////////////////////////
// DeviceBridge only instanciates the correct device bridge
/////////////////////////////////////////
import flash.system.Capabilities;

public class DeviceBridge
{
	// Change in audio output levels event
	static public const AUDIO_LEVEL_CHANGE:String	= "audioLevelChange";

	static private var _device:IDeviceBridge; // This will be of different class in different environments

        // Access the device bridge
	static public function get device():IDeviceBridge
	{
		if (!_device)
		{
			createDevice();
		}
		return _device;
	}

	static private function createDevice():void
	{
                // The following checks if we are currently running on PlayBook
		if (Capabilities.manufacturer == "Winchester")
		{
			_device = new PlayBookDevice;
		}
		else
		{
			_device = new DesktopDevice;
		}
	}
}

 

LinkedInShare
Posted in Uncategorized | Leave a comment