×

Implementation

For information about the design and functionality of the MIDlet, see section Design.

Game architecture

Figure 1. Architecture of the game

Game logic

The FrozenBubble class is a MIDlet class which basically just sets a FrozenCanvas object as the current displayable.

FrozenCanvas

FrozenCanvas is the heart of this application. It handles inputs and other notifications and also runs the main loop of the game using a Timer object. Depending on the state of the game, either a splash screen, a game screen or some menu screen is drawn. The splash screen is shown while resources are loaded. When the game screen is shown, the play method of the FrozenGame class is called to update the state of a game level before the game screen is drawn. In pause state the game screen is drawn but the play method is not called. When the menu button is pressed, the game goes to a main menu state and the menu is shown.

FrozenGame

The FrozenGame class stores the state of the game in a level. When play method is called, positions of moving bubbles, animations and so on. are updated. Initial positions of the bubbles are loaded using the level manager. This class uses the BubbleManager class to keep track of which colours are valid for new bubbles and the Sprite classes to draw bubbles and other elements. When a sound effect needs to played, the playSound method in sound manager is called.

Resizing images

The size of the game area is designed to be 320x480 pixels, but when the game loads, all graphics are resized so that the area fits inside a screen with any resolution. The aspect ratio is kept the same and any extra space is filled with background.

Figure 2. Red line shows the game area

Java ME does not provide any image scaling methods, so custom ones were implemented for this game. Pixel mixing method produces good results when images need to be scaled to smaller size and bilinear interpolation is a simple method when scaling to bigger size.

Figure 3. Original (left), pixel mixing (middle) and bilinear interpolation (right)

Pixel mixing

Figure 4. Red grid represents the original pixels and green grid represents the resized pixels

In the pixel mixing method each new pixel covers an area which may overlap several original pixels. Colour of a new pixel is an average of the colours of the original pixels, weighted by the area the new pixel covers the original pixel.

To speed up processing everything is implemented using integers.

private static Image pixelMixing(Image original,
        int newWidth, int newHeight) {
        int[] rawInput = new int[original.getHeight() * original.getWidth()];
        original.getRGB(rawInput, 0, original.getWidth(), 0, 0,
            original.getWidth(), original.getHeight());

        int[] rawOutput = new int[newWidth * newHeight];

        int oWidth = original.getWidth();
        int[] oX16 = new int[newWidth + 1];
        for (int newX = 0; newX <= newWidth; newX++) {
            oX16[newX] = ((newX * oWidth) << 4) / newWidth;
        }

        int[] oXStartWidth = new int[newWidth];
        int[] oXEndWidth = new int[newWidth];
        for (int newX = 0; newX < newWidth; newX++) {
            oXStartWidth[newX] = 16 - (oX16[newX] % 16);
            oXEndWidth[newX] = oX16[newX + 1] % 16;
        }

        int oHeight = original.getHeight();
        int[] oY16 = new int[newHeight + 1];
        for (int newY = 0; newY <= newHeight; newY++) {
            oY16[newY] = ((newY * oHeight) << 4) / newHeight;
        }

        int oX16Start, oX16End, oY16Start, oY16End;
        int oYStartHeight, oYEndHeight;
        int oXStart, oXEnd, oYStart, oYEnd;
        int outArea, outColorArea, outAlpha, outRed, outGreen, outBlue;
        int areaHeight, areaWidth, area;
        int argb, a, r, g, b;
        for (int newY = 0; newY < newHeight; newY++) {
            oY16Start = oY16[newY];
            oY16End = oY16[newY + 1];
            oYStart = oY16Start >>> 4;
            oYEnd = oY16End >>> 4;
            oYStartHeight = 16 - (oY16Start % 16);
            oYEndHeight = oY16End % 16;
            for (int newX = 0; newX < newWidth; newX++) {
                oX16Start = oX16[newX];
                oX16End = oX16[newX + 1];
                oXStart = oX16Start >>> 4;
                oXEnd = oX16End >>> 4;
                outArea = 0;
                outColorArea = 0;
                outAlpha = 0;
                outRed = 0;
                outGreen = 0;
                outBlue = 0;
                for (int j = oYStart; j <= oYEnd; j++) {
                    areaHeight = 16;
                    if (oYStart == oYEnd) {
                        areaHeight = oY16End - oY16Start;
                    }
                    else if (j == oYStart) {
                        areaHeight = oYStartHeight;
                    }
                    else if (j == oYEnd) {
                        areaHeight = oYEndHeight;
                    }
                    if (areaHeight == 0) {
                        continue;
                    }
                    for (int i = oXStart; i <= oXEnd; i++) {
                        areaWidth = 16;
                        if (oXStart == oXEnd) {
                            areaWidth = oX16End - oX16Start;
                        }
                        else if (i == oXStart) {
                            areaWidth = oXStartWidth[newX];
                        }
                        else if (i == oXEnd) {
                            areaWidth = oXEndWidth[newX];
                        }
                        if (areaWidth == 0) {
                            continue;
                        }

                        area = areaWidth * areaHeight;
                        outArea += area;
                        argb = rawInput[i + j * original.getWidth()];
                        a = (argb >>> 24);
                        if (a == 0) {
                            continue;
                        }
                        area = a * area;
                        outColorArea += area;
                        r = (argb & 0x00ff0000) >>> 16;
                        g = (argb & 0x0000ff00) >>> 8;
                        b = argb & 0x000000ff;
                        outRed += area * r;
                        outGreen += area * g;
                        outBlue += area * b;
                    }
                }
                if (outColorArea > 0) {
                    outAlpha = outColorArea / outArea;
                    outRed = outRed / outColorArea;
                    outGreen = outGreen / outColorArea;
                    outBlue = outBlue / outColorArea;
                }
                rawOutput[newX + newY * newWidth] = (outAlpha << 24)
                    | (outRed << 16) | (outGreen << 8) | outBlue;
            }
        }
        return Image.createRGBImage(rawOutput, newWidth, newHeight, true);
    }

Bilinear interpolation

In the billinear interpolation method colour of a new pixel is an weighted average of the four closest pixels in the original image (see Bilinear interpolation - Wikipedia).

private static Image bilinearInterpolation(Image original,
        int newWidth, int newHeight) {
        int[] rawInput = new int[original.getHeight() * original.getWidth()];
        original.getRGB(rawInput, 0, original.getWidth(), 0, 0, original.
            getWidth(), original.getHeight());

        int[] rawOutput = new int[newWidth * newHeight];

        int oWidth = original.getWidth();
        int[] oX16 = new int[newWidth];
        int max = (oWidth - 1) << 4;
        for (int newX = 0; newX < newWidth; newX++) {
            oX16[newX] = ((((newX << 1) + 1) * oWidth) << 3) / newWidth - 8;
            if (oX16[newX] < 0) {
                oX16[newX] = 0;
            }
            else if (oX16[newX] > max) {
                oX16[newX] = max;
            }
        }

        int oHeight = original.getHeight();
        int[] oY16 = new int[newHeight];
        max = (oHeight - 1) << 4;
        for (int newY = 0; newY < newHeight; newY++) {
            oY16[newY] = ((((newY << 1) + 1) * oHeight) << 3) / newHeight - 8;
            if (oY16[newY] < 0) {
                oY16[newY] = 0;
            }
            else if (oY16[newY] > max) {
                oY16[newY] = max;
            }
        }

        int[] oX = new int[2];
        int[] oY = new int[2];
        int[] wX = new int[2];
        int[] wY = new int[2];
        int outWeight, outColorWeight, outAlpha, outRed, outGreen, outBlue;
        int w, argb, a, r, g, b;
        for (int newY = 0; newY < newHeight; newY++) {
            oY[0] = oY16[newY] >>> 4;
            wY[1] = oY16[newY] & 0x0000000f;
            wY[0] = 16 - wY[1];
            oY[1] = wY[1] == 0 ? oY[0] : oY[0] + 1;
            for (int newX = 0; newX < newWidth; newX++) {
                oX[0] = oX16[newX] >>> 4;
                wX[1] = oX16[newX] & 0x0000000f;
                wX[0] = 16 - wX[1];
                oX[1] = wX[1] == 0 ? oX[0] : oX[0] + 1;

                outWeight = 0;
                outColorWeight = 0;
                outAlpha = 0;
                outRed = 0;
                outGreen = 0;
                outBlue = 0;
                for (int j = 0; j < 2; j++) {
                    for (int i = 0; i < 2; i++) {
                        if (wY[j] == 0 || wX[i] == 0) {
                            continue;
                        }
                        w = wX[i] * wY[j];
                        outWeight += w;
                        argb = rawInput[oX[i] + oY[j] * original.getWidth()];
                        a = (argb >>> 24);
                        if (a == 0) {
                            continue;
                        }
                        w = a * w;
                        outColorWeight += w;
                        r = (argb & 0x00ff0000) >>> 16;
                        g = (argb & 0x0000ff00) >>> 8;
                        b = argb & 0x000000ff;
                        outRed += w * r;
                        outGreen += w * g;
                        outBlue += w * b;
                    }
                }
                if (outColorWeight > 0) {
                    outAlpha = outColorWeight / outWeight;
                    outRed = outRed / outColorWeight;
                    outGreen = outGreen / outColorWeight;
                    outBlue = outBlue / outColorWeight;
                }
                rawOutput[newX + newY * newWidth] = (outAlpha << 24)
                    | (outRed << 16) | (outGreen << 8) | outBlue;
            }
        }
        return Image.createRGBImage(rawOutput, newWidth, newHeight, true);
    }

Saving/restoring state

The state of the game is saved when the game is closed. The destroyApp method in FrozenBubble is called and it calls cleanUp method in GameView.

protected void destroyApp(boolean bln) {
        if (gameView != null) {
            gameView.cleanUp();
        }
        gameView = null;
    }

    public void cleanUp() {
        thread.saveState();
        thread.cleanUp();
    }

The saveState method in GameThread saves the state of the different components to RMS.

public synchronized void saveState() {
            try {
                RecordStore gameState = RecordStore.openRecordStore("GameState",
                    true);
                if (gameState.getNumRecords() == 0) {
                    gameState.addRecord(null, 0, 0);
                }
                ByteArrayOutputStream bout = null;
                try {
                    bout = new ByteArrayOutputStream();
                    DataOutputStream dout = new DataOutputStream(bout);
                    synchronized (gameCanvas) {
                        frozenGame.saveState(dout);
                        levelManager.saveState(dout);
                        FrozenBubble.saveSettings(dout);
                        lifeManager.saveState(dout);
                    }
                    byte[] data = bout.toByteArray();
                    gameState.setRecord(getRecordId(gameState), data, 0,
                        data.length);
                    gameState.closeRecordStore();
                }
                catch (IOException e) {
                }
                finally {
                    try {
                        if (bout != null) {
                            bout.close();
                        }
                    }
                    catch (IOException e) {
                    }
                }
            }
            catch (Exception e) {
                try {
                    RecordStore.deleteRecordStore("GameState");
                }
                catch (RecordStoreException rse) {
                }
            }
        }

        private int getRecordId(RecordStore store)
            throws RecordStoreException {
            RecordEnumeration e = store.enumerateRecords(null, null, false);
            try {
                return e.nextRecordId();
            }
            finally {
                e.destroy();
            }
        }

The state is restored when the game is restarted and the GameView game canvas is shown for the first time.

protected void showNotify() {
        final boolean firstStart = !thread.isAlive();
        if (firstStart) {
            thread.start();
        }
        thread.setCanvasSize(getWidth(), getHeight());
        thread.setGraphics(getGraphics());
        if (firstStart) {
            thread.restoreState();
        }
    }

The restoreState method in GameThread restores the state of the different components from RMS.

public synchronized void restoreState() {
            stateLoaded = false;
            try {
                RecordStore gameState = RecordStore.openRecordStore("GameState",
                    true);
                if (gameState.getNumRecords() != 0) {
                    try {
                        DataInputStream din =
                            new DataInputStream(new ByteArrayInputStream(gameState.
                            getRecord(getRecordId(gameState))));
                        synchronized (gameCanvas) {
                            frozenGame.restoreState(din, imageList);
                            levelManager.restoreState(din);
                            FrozenBubble.restoreSettings(din);
                            lifeManager.restoreState(din);
                        }
                    }
                    catch (IOException e) {
                    }
                }
                gameState.closeRecordStore();
            }
            catch (RecordStoreException e) {
            }
            stateLoaded = true;
        }

Playing sounds

In Java ME the number of prefetched audio samples and the number of currently playing samples is limited. loadedPlayers and playing vectors keep track of which sounds are loaded and playing, so that when a new sound needs to be loaded or played, the least recently used one is discarded.

private final static int MAX_LOADED_PLAYERS = 8;
    private final static int MAX_PLAYERS = supportsMixing() ? 3 : 1;
    private final Vector loadedPlayers = new Vector();
    private final Vector playing = new Vector();

The supportsMixing method tries to guess if the device supports mixing.

public static boolean supportsMixing() {
        String s = System.getProperty("supports.mixing");
        return s != null && s.equalsIgnoreCase("true")
            && !FrozenBubble.isS60Phone();
    }

When a sound needs to be played and the sound is already loaded, it is restarted. If the sound is not loaded, first the limitLoadedPlayers method is called to make room for a new sound in the loadedPlayers vector and then a new player is created and started.

public final void playSound(int sound) {
        if (FrozenBubble.getSoundOn()) {
            if (restart(sound)) {
                return;
            }
            Player player = null;
            InputStream stream =
                SoundManager.class.getResourceAsStream(resources[sound]);
            try {
                limitLoadedPlayers();
                player = Manager.createPlayer(stream, "audio/wav");
                player.realize();
                player.prefetch();
                player.addPlayerListener(null);
                start(sound, player);
            }
            catch (Exception e) {
            }
        }
    }

The restart method tries to find the sound from the loadedPlayers vector. If the sound is found, the loadedPlayers vector is updated so that the players are in playing order in the vector and the found player is restarted.

    private synchronized boolean restart(int sound) {
        synchronized (loadedPlayers) {
            for (int i = 0; i < loadedPlayers.size(); i++) {
                SoundPlayer sp = (SoundPlayer) loadedPlayers.elementAt(i);
                if (sp.sound == sound) {
                    loadedPlayers.removeElement(sp);
                    loadedPlayers.addElement(sp);
                    stop(sp.player);
                    try {
                        sp.player.setMediaTime(0);
                    }
                    catch (Exception e) {
                    }
                    start(sp.player);
                    return true;
                }
            }
            return false;
        }
    }

The limitLoadedPlayers method cleans up players, so that after the method has run there is room for a new player.

private void limitLoadedPlayers() {
        synchronized (loadedPlayers) {
            try {
                while (loadedPlayers.size() >= MAX_LOADED_PLAYERS) {
                    SoundPlayer sp = (SoundPlayer) loadedPlayers.firstElement();
                    clean(sp);
                }
            }
            catch (Exception e) {
            }
        }
    }

    private void clean(SoundPlayer sp) {
        synchronized (loadedPlayers) {
            loadedPlayers.removeElement(sp);
            stop(sp.player);
            try {
                sp.player.deallocate();
                sp.player.close();
            }
            catch (Exception e) {
            }
        }
    }

The start method which takes sound and a new player as arguments adds the player to loadedPlayers vector and starts playback.

private synchronized void start(int sound, Player player) {
        synchronized (loadedPlayers) {
            loadedPlayers.addElement(new SoundPlayer(sound, player));
            start(player);
        }
    }

The start and stop methods control how many players are concurrently playing.

private void start(Player player) {
        synchronized (playing) {
            try {
                while (playing.size() >= MAX_PLAYERS) {
                    Player p = (Player) playing.firstElement();
                    playing.removeElementAt(0);
                    stop(p);
                }
            }
            catch (Exception e) {
            }
            playing.addElement(player);
            try {
                player.start();
            }
            catch (Exception e) {
            }
        }
    }

    private void stop(Player player) {
        synchronized (playing) {
            playing.removeElement(player);
            try {
                if (player.getState() == Player.STARTED) {
                    try {
                        player.stop();
                    }
                    catch (Exception e) {
                    }
                }
            }
            catch (Exception e) {
            }
        }
    }

Custom fonts

The BubbleFont class uses bitmaps to draw custom fonts. All the supported characters are defined with an array.

private static final char[] CHARACTERS = {
        '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*',
        '+', ',', '-', '.', '/', '0', '1', '2', '3', '4',
        '5', '6', '7', '8', '9', ':', ';', '<', '=', '>',
        '?', '@', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
        'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
        's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '{',
        '}', '[', ']', ' ', '\\', ' ', ' '};

The paintChar method finds the index of a character using the getCharIndex method, and if the character is supported and a bitmap for the character is loaded, the bitmap is drawn and the width of the character is returned so that a next character can be drawn right after this one.

public final int paintChar(char c, int x, int y, Graphics g,
        double scale, int dx, int dy) {
        if (c == ' ') {
            return SPACE_CHAR_WIDTH + SEPARATOR_WIDTH;
        }
        int index = getCharIndex(c);
        if (index == -1 || fontMap[index].bmp == null) {
            return 0;
        }
        int imageWidth = (int) (fontMap[index].bmp.getWidth() / scale + 0.5);

        Sprite.drawImage(fontMap[index], x, y, g, scale, dx, dy);

        return imageWidth + SEPARATOR_WIDTH;
    }

    private int getCharIndex(char c) {
        for (int i = 0; i < CHARACTERS.length; i++) {
            if (CHARACTERS[i] == c) {
                return i;
            }
        }

        return -1;
    }

The print method draws a string using the paintChar method.

public final void print(String s, int x, int y, Graphics canvas,
        double scale, int dx, int dy) {
        int len = s.length();
        for (int i = 0; i < len; i++) {
            char c = s.charAt(i);
            x += paintChar(c, x, y, canvas, scale, dx, dy);
        }
    }

Last updated 24 October 2013

Back to top

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×