Android Tutorial PacDroid: from scratch to fun

PacDroid: from scratch to fun

In this tutorial, I'm going to explain how to write a game from scratch with libGDX. I will make reference to my tutorials “How to make games” and “Introduction to libGDX”, so I suppose that you have read them.

I chose to do a clone of Pac-Man, a game released in 1980 where a voracious yellow circle has to eat all food dots in a maze while avoiding four ghosts trying to kill it. Four “energizers” in each corner allow Pac-Man to reverse the situation and make the ghosts edible for a short duration. The original game looked like this:
Pac-man.png


Note: This tutorial and the project to download contain copyrighted materials that I'm allowed to use for educational purposes. Don't reuse them in a published work. It's illegal.

Preparation

Before writing the first line of code, I need to create a few images: a droid, four ghosts and a maze. This is quickly done; I will spend later more time on the artwork. These first drawings should help define the right size of each element and, of course, allow to build our first prototype. At this stage, all sizes depend on an important decision to take: how will PacDroid be controlled? If I decide to use a touchpad, I will have less space on screen for the maze and other graphic elements. I know that the maze will take a lot of space, so I prefer to use only the accelerometer. Maybe later I will implement a transparent touchpad for the gamers uncomfortable with the accelerometer and for the (very rare) devices without this sensor.

Here are my first drawings:
draft.png


PacDroid is going to be animated so I need to draw two frames: mouth opened and mouth closed, and my frames should face the four directions.
In the original version, the ghosts’ eyes are also animated. That won’t be the case here. It’s a tutorial, not a real game, so I avoid overloading it.

For the maze, I decide to make a tiled map, as in the original version. I construct a tile set by drawing over a screenshot, and then import it in Tiled to create the first level:
maze_draft.png

The properties panel on the right is oversized to make it readable.

doorX and doorY are the coordinates of the left tile of the exit of the ghosts house, and startX and startY are the coordinates of the starting position of PacDroid. By storing these properties in the map file, I separate the work done on levels from the code (if I worked in a company with a level designer, he/she could decide freely where PacDroid starts, where to place the ghosts’ house, etc.; nothing is hardcoded).

The tile size is 37 pixels (width and height) and the map size is 28 tiles x 31. So my map has a width = 28 * 37 = 1036 pixels and a height = 31 * 37 = 1174 pixels. These dimensions will have to be slightly scaled down on a WXGA tablet (1280x800), and more importantly on an ordinary WVGA phone (800x480) but in both cases the quality should not suffer.

I have to decide how to draw my food dots: directly in the map editor, or with sprites rendered in real time. The first solution has an important disadvantage (it’s complicated to change the appearance of dots and to set effects on them), but the second has an even greater drawback: it would require hundreds of sprite and an array to memorize the initial locations and the eaten dots. I don’t want to bother with that so I decide to add the food dots and the energizers directly on the map. Because of this choice, the energizers (the big circle in the top left corner of the image below) won't flash as in the original version. Note that I could have done an exception for the four energizers, but I want to process all tiles with the same method in this tutorial.

with_dots.png


After placing the dots, I count them and I add a property Goal to my level. PacDroid must eat “Goal” dots to go to the next level.

I pack all my images, except the tile set, in a texture atlas with the texture packer.

It's time to write the game logic. As Pac-Man is a well-known game and there are plenty of web sites about it, I don't really need to formalize the concept and write the rules. I can read them here if needed. But I still write the pseudo-code that I will insert in the code as comments.
Example:
B4X:
If the droid collides with a ghost then
      If the ghost can be eaten then
            The player gets extra points
            The earned points are shown above the eaten ghost
            The ghost appearance changes
            The ghost returns to the house (new mode = Return)
      Else
            The droid has been caught -> the game is paused
            The droid death is animated
            The death music is played
            The number of lives is decreased
            If no more lives then
                  Game over
            Else
                  The actors come back to their starting position
            End if
      End if
End if

First lines of code: the skeleton

The first thing I write is the declaration of libGDX in Globals and in the Activity events:
B4X:
Sub Globals
      Dim lGdx As LibGDX
      Dim GL As lgGL
End Sub

Sub Activity_Create(FirstTime As Boolean)
      Dim Config As lgConfiguration

      'Enables the accelerometer
      Config.useAccelerometer = True

      'Disables the compass
      Config.useCompass = False

      'Forces the device to stay on
      Config.useWakelock = True

      'Limits the number of simultaneous sounds
      Config.maxSimultaneousSounds = 4

      'Creates the libGDX surface
      lGdx.Initialize2(Config, "LG")
End Sub

Sub Activity_Resume
      'Informs libGDX of Resume events
      If lGdx.IsInitialized Then lGdx.Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
      'Informs libGDX of Pause events
      If lGdx.IsInitialized Then lGdx.Pause
End Sub

I force the device to stay on by creating a wake lock because of the accelerometer; there will be no activity on the touch screen during the game.
I know that my game does not need a lot of simultaneous sounds, so I limit them to 4.

The next step is to write the events of the libGDX life-cycle, then to think to the organization of my code. As I explained in the first tutorial, it’s better to create a class for each element: clsDroid, clsGhost and clsMaze. And because a scene graph will make my life easier, I declare a lgScn2DStage in Globals and in the Create event of libGdx. I don’t forget to dispose it in the Dispose event. I add a main lgScn2DTable as advised in the second tutorial and a capture listener to the stage so that I can filter all the input events before they reach the Scene2D actors. Now my LG_Create event contains:
B4X:
Sub LG_Create
      'Initializes the stage
      Stage.Initialize("")

      'Adds a capture listener to the stage
      Dim IL As lgScn2DInputListener
      IL.Initialize("ST")
      Stage.AddCaptureListener(IL)

      'Creates the main table
      Table.Initialize("")
      Table.FillParent = True
      Stage.AddActor(Table)
End Sub

I add the code in the Render event to perform the actions of actors and to draw the actors:
B4X:
Sub LG_Render
      'Clears the screen
      GL.glClearColor(0, 0, 0, 1)
      GL.glClear(GL.GL10_COLOR_BUFFER_BIT)

      'Applies the actions to actors
      Stage.Act

      'Draws the actors
      Stage.Draw
End Sub

I could have gone faster by using the _TemplateScene2DTable.b4a which contains already all this code.

As I have a lot of things to do before drawing my actors, I insert in the Render event a call to another sub: “LG_Update”. I will place all the game logic in this sub.

Ok, it’s time to add some flesh to the skeleton. I’m going to convert my pseudo-code in Basic in a specific order: first the loading code, second the drawing code, third the game logic. I want to see quickly how my actors are rendered and whether the chosen layout (with the score and highscore above, the maze in the middle and the lives below) adapts well to different screen sizes.

Loading and initialization

The loading is done in the Create event without an Asset Manager because I don’t have many resources or big ones that I should wait for. Some resources are loaded and initialized directly in the event or with the Initialize function of the corresponding actor. While writing these Initialize functions, I realize that it would be easier to position my two moving actors (droid and ghost) with the 0, 0 coordinates in the bottom left corner of the maze. As I know that actors placed in a group use the local coordinates system of their parent, I create a lgScn2DGroup that I place in the middle cell of my main table, where the maze takes place, and I will add my actors to this group when needed. At the beginning, my moving actors are in the backstage because I want to display a title over an empty maze.

The score and highscore labels are two lgScn2DLabel, each one in a cell. The score cell is expanded to push the highscore label against the right side. The maze cell, in the next row, has to occupy all the space available, so I call Expand and set Colspan to 2 (otherwise my cell couldn’t be larger than the score cell).

While I create the bottom cell to show the lives, I find that it would be a good idea to add another table in this cell so that I can draw lives in separate cells of this new table. To decrease visually the number of lives, I will just have to remove the last cell of the table.
For the life image, I will reuse the second frame of PacDroid (open mouth).

To load the maze, I initialize a lgMapTmxMapLoader. Then I size the tiles so as the whole maze fits into the screen and no space is lost. To do my computations, I need to know the size of the map and the size of a tile. I read them in the properties of the map:
B4X:
MapWidth = Maps.Properties.Get("width")
MapHeight = Maps.Properties.Get("height")
Dim TileWidth As Int = Maps.Properties.Get("tilewidth")
Dim TileHeight As Int = Maps.Properties.Get("tileheight")
Dim RatioW As Float = Width / (TileWidth * MapWidth)
Dim RatioH As Float = (Height - (GutterSize * 2)) / (TileHeight * MapHeight)
If RatioW < RatioH Then
      MapRenderer.Initialize4(Maps, RatioW, SpriteBatch)
      TileSize = MapRenderer.UnitScale * TileWidth
Else
      MapRenderer.Initialize4(Maps, RatioH, SpriteBatch)
      TileSize = MapRenderer.UnitScale * TileHeight
End If
In this code, GutterSize is the space taken by the score labels on top and by the lives at the bottom.

Let’s load the PacDroid frames now.
I did not create the frames facing to the left and to the top. It’s easy to create them at runtime by flipping the other frames:
B4X:
argRightAnim = Atlas.FindRegions2("right")
argDownAnim = Atlas.FindRegions2("down")
For i = 0 To 1
      argLeftAnim(i).Initialize(argRightAnim(i))
      argLeftAnim(i).Flip(True, False)
      argUpAnim(i).Initialize(argDownAnim(i))
      argUpAnim(i).Flip(False, True)
Next

My frames have to be scaled to the current resolution. To ensure they are scaled properly for the current tile size, I compute the ratio between the tile size and the frame size and I use it as the scale. I ensure also that the origin is in the middle of the image (so the scaling will be done around this point). Note that my frames are two times bigger than a tile, but will be centered on one tile. It’s different from the original version.
B4X:
Actor.ScaleX = Main.Maze.TileSize / argRightAnim(0).OriginalWidth * 2
Actor.ScaleY = Main.Maze.TileSize / argRightAnim(0).OriginalHeight * 2
Actor.OriginX = argRightAnim(0).OriginalWidth / 2
Actor.OriginY = argRightAnim(0).OriginalHeight / 2

To create the ghosts, I retrieve their image from the atlas as a sprite, scale it with the same scale as PacDroid, then create an image actor. The image actor expects a drawable, so I convert the sprite:
B4X:
Dim sdwGhost As lgScn2DSpriteDrawable
sdwGhost.Initialize2(sprGhost)
Actor.InitializeWithDrawable(sdwGhost, "")

All my resources are now loaded and initialized, let’s draw them.

Rendering

To position properly my moving actors, I prefer to use coordinates in tiles rather than in pixels. If the coordinate value is an integer, the actor is over one tile. If the value is a float, the actor is across two tiles. In all cases, it has to end its move on a tile, so with integer values. To convert between pixels and tiles, I write a set of functions. Example:
B4X:
Public Sub FromTileToPixel_X(TileX As Int) As Int
      Return Round((TileX - 0.5) * TileSize)
End Sub

Public Sub FromTileToPixel_Y(TileY As Int) As Int
      Return Round((TileY - 0.5) * TileSize)
End Sub

Public Sub FromTileToPixel_InvertedY(TileY As Int) As Int
      Return Round((MapHeight - 1.5 - TileY) * TileSize)
End Sub
I have an offset of 0.5 due to the size of my actors (they are bigger than tiles). And as the map editor uses an Y-axis pointing downwards, which is the contrary here, I have to invert the Y coordinates stored in the level properties, hence the FromTileToPixel_InvertedY function.

All the rendering is done by the SpriteBatch of the stage. When Stage.Draw is called, it raises the Draw event of my group of actors, in the middle cell. I have nothing to do in this event apart calling the specific Draw function of the maze (it won’t be called automatically because I did not declare explicitly the maze as a Scene2D actor). I disable the blending before drawing because the maze has no transparent tile and thus no blending is needed. It’s just an optimization; it’s not required.
To avoid creating multiple tiled maps, I put each different design of the maze (each level) in a different layer of the map. That’s why, in the Draw function of the maze, I render only the layer which corresponds to the current level:
B4X:
Public Sub Draw(Camera As lgOrthographicCamera)
      MapRenderer.SetCameraView(Camera)
      MapRenderer.RenderTileLayer(CurrentLayer)
End Sub

The ghosts are automatically rendered because they are image actors. PacDroid, on the contrary, is a basic actor, so I have to render it myself. The main reason for this is because it is animated. I have to select the appropriate frame to render and this frame must be positioned exactly. When I packed the frames of PacDroid in the atlas, I chose the option to remove their whitespace, so now they have different sizes. Fortunately, the atlas kept track of the amount of space removed and returns this information in the offset property of the lgTextureAtlasRegion:
B4X:
SpriteBatch.DrawRegion3(Frame, _
                        Actor.X + (Frame.OffsetX * Actor.ScaleX) - Actor.OriginX + Main.Maze.TileSize, _
                        Actor.Y + (Frame.OffsetY * Actor.ScaleY) - Actor.OriginY + Main.Maze.TileSize, _
                        Actor.OriginX, Actor.OriginY, Frame.RegionWidth, Frame.RegionHeight, _
                        Actor.ScaleX, Actor.ScaleY, 0)

The lgAnimation class used to animate PacDroid is fairly simple to use. It is initialized with two parameters: the array of two frames corresponding to the current movement direction and the duration between each frame, which is 300ms in this game:
B4X:
Anim.Initialize(FrameDuration, argRightAnim)
In the Draw event, I select the frame by indicating the elapsed time since the beginning of the animation (the “state time”) and that’s all.
B4X:
Dim Frame As lgTextureAtlasRegion = Anim.GetKeyFrame2(StateTime, True)
StateTime = StateTime + Main.DeltaTime

After placing my actors for a first rendering, here are the results on a Nexus 7:
PacDr0_mini.png


We have now something that looks like a game but it does not react to the user input and the actors do not move. I’m going to bring life to all this. I begin by PacDroid.

......
 
Last edited:

yontala

Member
Licensed User
Longtime User
That's amazing! It's the best tutorial!!!
I have a Question...If I develop a game using: LibGDX, Tiled and Texture Packer.
I have to paid for used it or they are for free? Can I sell this game?
 

sorex

Expert
Licensed User
Longtime User
the point is to learn from it and start making your own game (not selling the work of someone else)
 

Informatix

Expert
Licensed User
Longtime User
That's amazing! It's the best tutorial!!!
I have a Question...If I develop a game using: LibGDX, Tiled and Texture Packer.
I have to paid for used it or they are for free? Can I sell this game?
All these products are free and people made them to allow you to create any kind of app, including commercial ones, full of ads.
You have to mention that you used some of them in your credits/license screen. Read their license.
 

yontala

Member
Licensed User
Longtime User
Thanks, I'm agree. When I say "if can I sell this game" I refer to developing a game using LibGDX, Tiled and Texture Packer from Zero.
I apologize: my english is not good..
 

johnl

Member
Licensed User
Longtime User
When I try to run pacdroid I get the below. Sorry if it's obvious, but any ideas? I'm using the latest version of B4A, and libgdx from here: https://www.b4x.com/android/forum/threads/libgdx-game-engine.32594/



mainonCreate (java line: 35)



java.lang.NoClassDefFoundError: anywheresoftware/b4a/libgdx/graphics/lgSpriteBatch



at java.lang.Class.getDeclaredMethods(Native Method)

at java.lang.Class.getDeclaredMethods(Class.java:656)

at anywheresoftware.b4a.BA.loadHtSubs(BA.java:419)

at flm.b4a.pacdroid.main.onCreate(main.java:35)

at android.app.Activity.performCreate(Activity.java:5600)

at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1093)

at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2504)

at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2599)

at android.app.ActivityThread.access$900(ActivityThread.java:174)

at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1321)

at android.os.Handler.dispatchMessage(Handler.java:102)

at android.os.Looper.loop(Looper.java:146)

at android.app.ActivityThread.main(ActivityThread.java:5748)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)

at dalvik.system.NativeStart.main(Native Method)

Caused by: java.lang.ClassNotFoundException: Didn't find class "anywheresoftware.b4a.libgdx.graphics.lgSpriteBatch" on path: DexPathList[[zip file "/data/app/flm.b4a.pacdroid-2.apk"],nativeLibraryDirectories=[/data/app-lib/flm.b4a.pacdroid-2, /vendor/lib, /system/lib]]

at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)

at java.lang.ClassLoader.loadClass(ClassLoader.java:497)

at java.lang.ClassLoader.loadClass(ClassLoader.java:457)

... 18 more
 

Informatix

Expert
Licensed User
Longtime User
When I try to run pacdroid I get the below. Sorry if it's obvious, but any ideas? I'm using the latest version of B4A, and libgdx from here: https://www.b4x.com/android/forum/threads/libgdx-game-engine.32594/



mainonCreate (java line: 35)



java.lang.NoClassDefFoundError: anywheresoftware/b4a/libgdx/graphics/lgSpriteBatch



at java.lang.Class.getDeclaredMethods(Native Method)

at java.lang.Class.getDeclaredMethods(Class.java:656)

at anywheresoftware.b4a.BA.loadHtSubs(BA.java:419)

at flm.b4a.pacdroid.main.onCreate(main.java:35)

at android.app.Activity.performCreate(Activity.java:5600)

at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1093)

at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2504)

at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2599)

at android.app.ActivityThread.access$900(ActivityThread.java:174)

at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1321)

at android.os.Handler.dispatchMessage(Handler.java:102)

at android.os.Looper.loop(Looper.java:146)

at android.app.ActivityThread.main(ActivityThread.java:5748)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)

at dalvik.system.NativeStart.main(Native Method)

Caused by: java.lang.ClassNotFoundException: Didn't find class "anywheresoftware.b4a.libgdx.graphics.lgSpriteBatch" on path: DexPathList[[zip file "/data/app/flm.b4a.pacdroid-2.apk"],nativeLibraryDirectories=[/data/app-lib/flm.b4a.pacdroid-2, /vendor/lib, /system/lib]]

at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)

at java.lang.ClassLoader.loadClass(ClassLoader.java:497)

at java.lang.ClassLoader.loadClass(ClassLoader.java:457)

... 18 more
No idea. Either you didn't put the libGdx library at the right place (and a lot of things are displayed in red in the IDE) or it's an unknown error. Ask to Erel in this case.
 

johnl

Member
Licensed User
Longtime User
Thanks for the quick reply. No, there's nothing in red. I've looked at a couple of the other examples and they worked fine. hmm.
 

Laurent95

Active Member
Licensed User
Longtime User
I think once I saw the source code of the pacdroid tutorial. Where to download?

You must read all ! Seems you didn't do it.
Take a look at second post of Informatix "PacDroid is alive!".
 

Laurent95

Active Member
Licensed User
Longtime User
Top