Thursday, February 21, 2013

Designing Cross-Platform Image Buffers

This post documents a design issue, some rationale, and the chosen solution. 

The Problem

As I was porting functionality from the old MHFramework to the new one, I realized that I needed a platform-independent solution for generating images from buffers.  Since the old engine was based entirely on Java's Abstract Window Toolkit (AWT) which isn't available on Android, I needed to find a way to accomplish the same thing for both of those platforms in a consistent manner.

Proposed Solutions

The first solution that came to mind was to encapsulate graphics contexts (MHGraphicsCanvas) into the the MHBitmapImage classes just like AWT does.  However, as with all such decisions, it comes with some immediate advantages and disadvantages.

Pros:

  • Our engine is never going to use a graphics context for anything other than drawing to a buffered bitmap.  Combining these classes would hide the coupling.  This cleans up the class structure of the  platform layer and also greatly simplifies the implementation of more advanced visual effects.
  • AWT and Android sort of reverse the association between bitmaps and canvases, and this would encapsulate those differences internally so we'd have a uniform way to work with image data.  (AWT's Image has Graphics, and Android's Canvas has a Bitmap, so even though they're semantically equivalent, their compositions are inverted.)

Cons:

  • MHBitmapImage no longer just stores image data.  It now also provides an interface for manipulating that data, so we may be in violation of the Single Responsibility Principle.
  • Not all image data requires a graphics context until it's rendered, so this could incur some memory overhead.
  • HOWEVER, we can solve both problems through composition and lazy instantiation.  Besides, this relationship already exists at the platform level anyway.

The Chosen Solution

 I decided to keep MHBitmapImage and MHGraphicsCanvas as separate classes, but I removed MHPlatform's factory method for creating a graphics canvas.  Now the only way to retrieve a canvas for drawing is to extract it from the image object.  Now you always have immediate access to the results of every rendering operation.

For example, the double-buffered rendering now happens by using an MHBitmapImage as the back buffer, and then passing its associated MHGraphicsCanvas to the screen manager.  When the call returns, the bitmap image is presented physically to the screen device.

I am happy with this solution.  Although I was unable to satisfactorily eliminate a class, I feel very comfortable with the design principles involved and the improvement in general usability of these critical elements.
 

Tuesday, February 19, 2013

Getting Started With MHFramework 3

Though still in the early stages of development, MHFramework 3 is vastly different from its predecessors in a variety of ways.  However, those prior versions had certain strengths that I desperately want to maintain as we go forward with the engine's redesign.  One of those strengths is the simplicity of the initial setup.

One of the highest priority design goals of MHF3 is cross-platform portability between Android and PC-based platforms.  With this in mind to help guide my design decisions, the initial setup for both platforms follows a simple, three-step process, with only a slight modification to the Android version:
  1. Create at least one screen.  This is done by inheriting from the engine's MHScreen class. (More on this in a future post.)
  2. Define your display settings by initializing an MHVideoSettings object.
  3. Pass those things into the engine along with the window in which your game app will run.  This is accomplished with a call to MHFramework.run().

Here's a PC-compatible example of a main class that accomplishes these things.

import javax.swing.JFrame;

import com.mhframework.MHFramework;
import com.mhframework.MHScreen;
import com.mhframework.MHVideoSettings;


public class PlatformTestPCWindow
{
    public static void main(String[] args)
    {
        // Step 1:  The screen.
        MHScreen startingScreen = new TestScreen();
        
        // Step 2:  The video settings.
        MHVideoSettings displaySettings = new MHVideoSettings();
        displaySettings.displayWidth = 800;
        displaySettings.displayHeight = 480;

        // Step 3:  Run it!
        MHFramework.run(new JFrame(), startingScreen, displaySettings);
    }
}

The Android version takes a very similar approach, but with a few additional rules:
  1. The main class must inherit from the engine's MHAndroidActivity class, which is a specialization of Android's basic Activity class that adds additional support for MHF3's multithreading requirements. 
  2. Rather than perform those steps in main, your program must override the Activity.onCreate() method. 
  3. Since Android's orientation can be specified as portrait or landscape, this must be specified here as well.  
    • Future versions may simply add the orientation constant as a field in MHVideoSettings and default it to landscape.  This way, the Android version will use the exact same three steps with no additional requirements.

import android.content.pm.ActivityInfo;
import android.os.Bundle;

import com.mhframework.MHFramework;
import com.mhframework.MHScreen;
import com.mhframework.MHVideoSettings;
import com.mhframework.platform.android.MHAndroidActivity;

public class PlatformTestAndroid extends MHAndroidActivity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        // Initialize Android-specific properties.
        super.onCreate(savedInstanceState);
        this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        
        // Step 1:  The screen.
        MHScreen startingScreen = new TestScreen();
        
        // Step 2:  The video settings.
        MHVideoSettings displaySettings = new MHVideoSettings();
        displaySettings.displayWidth = 800;
        displaySettings.displayHeight = 480;

        // Step 3:  Run it!
        MHFramework.run(this, startingScreen, displaySettings);
    }
}