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:

Informatix

Expert
Licensed User
Longtime User
PacDroid is alive!

Getting the input from the accelerometer is very easy:
B4X:
Dim NewX As Float = lGdx.Input.AccelerometerX
Dim NewY As Float = lGdx.Input.AccelerometerY
Since the Render event is called continuously, I place the code in this event.

PacDroid cannot move in diagonal, so I ignore the direction with the smallest change and I check whether the change is important enough to be considered:
B4X:
If Abs(NewX) > Abs(NewY) Then
      If NewX <= -Accel_Threshold Then
            PacDroid.DirectionX = Direction_RIGHT
      Else If NewX >= Accel_Threshold Then
            PacDroid.DirectionX = Direction_LEFT
      End If
Else
      If NewY >= Accel_Threshold Then
            PacDroid.DirectionY = Direction_BOTTOM
      Else If NewY <= -Accel_Threshold Then
            PacDroid.DirectionY = Direction_TOP
      End If
End If

When PacDroid starts to move, it continues until it bumps into a wall or the player changes its direction. That’s why I store the new direction in a Direction variable. I will use this setting to move PacDroid until there’s a change.

I can move PacDroid and check whether the new position is valid, or I can test the new position before any move. I choose the second option. To do this, I convert the current position in tile coordinates, select the tile targeted by the move and check whether this tile allows the move.

Here’s the function to get a tile:
B4X:
Public Sub GetTile(TileX As Int, TileY As Int) As lgMapStaticTiledMapTile
      Dim Cell As lgMapTiledMapLayerCell = CurrentLayer.GetCell(TileX, TileY)
      If Cell = Null Then
            Return Null
      Else
            Return Cell.Tile
      End If
End Sub

As I gave a name to six tiles of my tile set (empty, food, door, house, teleport and energizer), Tiled stored their reference in the TMX file and thus I’m able to recognize the type of a given tile. If this tile is a wall, PacDroid must stop:
B4X:
Public Sub CanMoveTo(Tile As lgMapStaticTiledMapTile) As Boolean
      Return (Tile = RefFoodTile OR Tile = RefEnergizerTile OR _
              Tile = RefEmptyTile OR Tile = RefTeleportTile)
End Sub

If PacDroid is allowed to move, I re-initialize the animation with the frames corresponding to the new direction and I increase the current position with the speed of PacDroid multiplied by the time step of the game (which is the delta time between two frames):
B4X:
If Anim.GetKeyFrame(0) <> argUpAnim(0) Then
      Anim.Initialize(FrameDuration, argUpAnim)
End If
Actor.Y = Min(Actor.Y + Speed * Main.DeltaTime, Limit)
The Limit value ensures that PacDroid won’t be moved past the targeted tile.

If PacDroid is not allowed to move, I reset Direction to Direction_NONE.

When PacDroid moves, it can eat food dots and energizers, or collides with a ghost. It can also use the teleporter in one of the two corridors. In this case, I move it instantly to the other side of the maze:
B4X:
If Tile = Maze.RefTeleportTile Then
      If TileX < Maze.MapWidth / 2 Then
            PacDroid.Actor.X = Maze.FromTileToPixel_X(TileX + 26)
      Else
            PacDroid.Actor.X = Maze.FromTileToPixel_X(TileX - 26)
      End If

To remove a food dot or an energizer when eaten, I just replace the tile by an empty tile:
B4X:
Maze.CurrentLayer.GetCell(TileX, TileY).Tile = Maze.RefEmptyTile
I check also whether all food dots are eaten. If True, I reset everything for the next level.

To know whether PacDroid collides with a ghost, I don’t need a complicated solution. The bounding box of actors is a square (width = height), so I just have to check the distance between two actors to know whether their bounding boxes overlap. The formula to compute a distance is:
Sqrt(Power(v1.X – v2.X, 2) + Power(v1.Y – v2.Y, 2))
As Sqrt is an expensive operation, it is common to compute only the squared distance for a comparison. The Dst2 function of the lgMathVector2 class limits its computation to the squared distance, so I write my comparison function with it:
B4X:
Sub CollideWithGhost As Int
      Dim PosDroid As lgMathVector2
      PosDroid.Set(PacDroid.Actor.X, PacDroid.Actor.Y)
      Dim CollisionDistance As Float = Maze.TileSize * Maze.TileSize
      For i = 0 To Ghosts.Length - 1
            Dim PosGhost As lgMathVector2
            PosGhost.Set(Ghosts(i).Actor.X, Ghosts(i).Actor.Y)
            If PosDroid.Dst2(PosGhost) < CollisionDistance Then
                  Return i
            End If
      Next
      Return -1
End Sub
This function returns -1 when there’s no collision and the index of a ghost when a collision occurs.
There’s a faster way of doing this collision detection, but I wanted to show you that one.

Now, my PacDroid can move around the maze, eat food dots and… die by meeting a ghost. But there’s no fun. It’s time to put a brain in the ghosts and turn them into real threats.

The ghosts A.I.

In the original version, each ghost had its own personality, and thus a different behaviour. I could do the same because the algorithms used are rather simple, but for the readability of my code and to ease its understanding, I will use the same simplified algorithm for all ghosts. If you are interested by the original algorithms, they are explained on this web site.

Ghosts are in different modes depending on some events:
  • Scatter: this is the initial mode. Each ghost has a fixed target tile, each of which is located in a different corner of the maze. This causes the four ghosts to disperse to the corners whenever they are in this mode. After 7 seconds, the ghost changes to the Chase mode. A ghost cannot be more than 3 times in the Scatter mode for a given level.
  • Chase: in this mode, each ghost uses the PacDroid position to target a tile. After 20 seconds, it returns to the Scatter mode.
  • Frightened: this mode is triggered by PacDroid when it eats an energizer. The appearance of ghosts changes and they flee away from PacDroid because they become edible. The duration of this mode is shortened as the player progresses through the levels. The initial duration is 7 seconds.
  • Return: when a ghost is eaten by PacDroid in Frightened mode, it changes to the Return mode. It becomes semi-transparent, cannot collide with other actors and returns to the ghosts’ house. Once in the house, it reverts to the mode it was in before being frightened.
To handle the mode, I create four MODE constants and the following variables (self-explanatory):
B4X:
Dim Mode, PreviousMode As Byte
Dim ModeTime, PreviousModeTime As Float
Dim ScatterCount As Byte
Private ScatterTargetX, ScatterTargetY As Int

In the Create event, I select a different target for the Scatter mode of each ghost and I load a new drawable for the Frightened mode.

I implement all changes of mode in the LG_Update sub and in the clsGhost class, and I add the value of DeltaTime to ModeTime to memorize how much time each ghost has spent in a given mode.

Now I’m ready to write the A.I. functions to move the ghosts.

Contrary to PacDroid, a ghost cannot change its mind and turn back. When it moves in one direction, it can only change the axis of its move (from horizontal to vertical or from vertical to horizontal). That eases my work.

I first create the functions to check whether the next move is allowed.
The ghost can enter the house at start or while it is in Return Mode. If it meets another ghost, the move is considered as blocked; the ghost will have to turn back (it’s the only exception to the rule).
In Return mode, it can freely pass through PacDroid and the other ghosts.

I use the new functions to build an array (Moves) of possible moves (FindPossibleMoves function). Each index of the array is a direction. Example:
B4X:
Dim IsInTheHouse As Boolean = (CurrentTile = Main.Maze.RefHouseTile)
Moves(Main.Direction_RIGHT).Initialize
If MovingDirection <> Main.Direction_LEFT Then
      'Checks the tile on the right
      Dim Tile As lgMapStaticTiledMapTile = Main.Maze.GetTile(TileX + 1, TileY)
      If CanMoveTo(Tile, IsInTheHouse) AND IsNotOccupied(TileX + 1, TileY) Then
            Moves(Main.Direction_RIGHT).Possible = True
            Moves(Main.Direction_RIGHT).Coordinates.Set(TileX + 1, TileY)
      End If
End If
My Moves array has two properties: Possible (true or false) and Coordinates (of the tile where the move is allowed).

With this array, I can select the next move according to the current mode. If the ghost is between two tiles, I wait until the move is finished. When it’s ready for a new move, I compute the distance between the targeted tile and all possible tiles around the ghost. The tile to reach has a fixed position in Scatter mode, is the PacDroid position in Chase and Frightened modes, and is the position of the house door in Return mode. In all modes, except Frightened, the shortest distance indicates the tile for the next move. In the Frightened mode, it’s the farthest distance (the ghost flees):
B4X:
'Is the current move finished?
If ContinueMove Then Return

'Creates an array of possible moves
Dim Moves(4) As typMove = FindPossibleMoves

'Selects the path that leads the ghost away from the droid
Dim PosDroid As lgMathVector2
PosDroid.Set(Main.PacDroid.Actor.X, Main.PacDroid.Actor.Y)
Dim Farthest_Idx As Int = -1
For i = 0 To 3
      If Moves(i).Possible Then
            Moves(i).Coordinates.X = Main.Maze.FromTileToPixel_X(Moves(i).Coordinates.X)
            Moves(i).Coordinates.Y = Main.Maze.FromTileToPixel_Y(Moves(i).Coordinates.Y)
            Moves(i).Distance = PosDroid.Dst2(Moves(i).Coordinates)
            If Farthest_Idx = -1 OR Moves(i).Distance > Moves(Farthest_Idx).Distance Then
                  Farthest_Idx = i
            End If
      End If
Next
As you can see, I use once again a squared distance (Dst2) instead of the full (and unnecessary) computation.

Once I know where the ghost must go, I move it:
B4X:
If Shortest_Idx > -1 Then
      MovingDirection = Shortest_Idx
      TargetX = Moves(Shortest_Idx).Coordinates.X
      TargetY = Moves(Shortest_Idx).Coordinates.Y
      If MovingDirection = Main.Direction_LEFT Then
            Actor.X = Max(Actor.X - Main.GhostSpeed * Main.DeltaTime, TargetX)
      Else If MovingDirection = Main.Direction_RIGHT Then
            Actor.X = Min(Actor.X + Main.GhostSpeed * Main.DeltaTime, TargetX)
      Else If MovingDirection = Main.Direction_BOTTOM Then
            Actor.Y = Max(Actor.Y - Main.GhostSpeed * Main.DeltaTime, TargetY)
      Else If MovingDirection = Main.Direction_TOP Then
            Actor.Y = Min(Actor.Y + Main.GhostSpeed * Main.DeltaTime, TargetY)
      End If
Else
      'The ghost is blocked by another ghost so it is allowed to change direction
      MovingDirection = Main.Direction_NONE
End If
The formula is the same as for PacDroid. Only the speed is different.

Many things are not properly handled yet (new level, earned points, update of the score label, extra lives, death of PacDroid, new game, etc.) but my first prototype is ready. I can try it out and see whether the rules are correctly implemented and whether the game is fun to play. With this prototype, I can evaluate different settings for the initial difficulty.

Finalization

To finalize the game, I create specialized functions to manage the score and the lives, and I add four labels (one per ghost) to show the earned points above an eaten ghost. To animate this label, I use actions:
B4X:
lblExtraPoints(CollidGhost).X = Ghosts(CollidGhost).Actor.X
lblExtraPoints(CollidGhost).Y = Ghosts(CollidGhost).Actor.Y + Maze.TileSize
lblExtraPoints(CollidGhost).AddAction(Actions.Sequence2( _
                    Actions.MoveBy2(0, Maze.TileSize * 1.5, 1), Actions.RemoveActor))
grpMain.AddActor(lblExtraPoints(CollidGhost))

I use also actions for the title and for the PacDroid death:
B4X:
PacDroid.Actor.AddAction(Actions.Sequence2( _
                         Actions.ScaleTo2(0, 0, 1), Actions.CallSub("Actors_Reset", Null)))
As you can see, you can call another sub with an action. It’s convenient to reset the game only after the death animation is finished (and there’s no need of an event to be notified of this end).

And here’s an action again to animate the end of the Frightened mode:
B4X:
Dim BlinkSequence As lgScn2DAction = Actions.Forever(Actions.Sequence4( _
               Actions.Hide, Actions.Delay(0.04), Actions.Show, Actions.Delay(0.04)))
Ghosts(i).Actor.AddAction(BlinkSequence)
When you set an action like this one that loops until you stop it, ensure that the state at the end is the right one. That’s why I add at the beginning of each mode:
B4X:
If Not(Ghosts(i).Actor.Visible) Then
      Ghosts(i).Actor.Visible = True
End If

I add the (original) music and sounds with the lgMusic and lgSound classes. And, to these chip tunes, I add another reminder of the 80’s: a pixelated font (Emulogic) for my labels. The bitmap font is generated at runtime:
B4X:
Dim FG As lgFontGenerator
NormalFont = FG.CreateFont("font/emulogic.ttf", lGdx.Graphics.Width * 0.038, "HighScore0123456789 Tutnas")
BigFont = FG.CreateFont("font/emulogic.ttf", lGdx.Graphics.Width * 0.09, "PACDROI NEXTLVGMY?")

Tada! My game is finished!

Here’s the final result:
scrn0_mini.png


P.S.: It took me two full days to write entirely the game, make the four levels and transform the images found on Internet. Two days from scratch to fun.

If you can reach the 7th level, a surprise awaits you.

v1.1: I fixed a bug and made the changes to be compatible with libGDX v1.0.
 

Attachments

  • Pacdroid_1_1.zip
    234.7 KB · Views: 1,699
Last edited:

Theera

Well-Known Member
Licensed User
Longtime User
Awesome,I love it.
 

robbies

New Member
Licensed User
Longtime User
Great tutorial. Thanks. There is a hell of a lot to get my head around. I've just tried to run Pacdroid, but for some reason I am getting an error. In the log for B4A is says "Ignoring event: lg_render. raised from the wrong thread.". I've tried it on my tablet running Jelly Bean 4.2.2, and on my phone running Gingerbread 2.3.6. I'm using B4A version 3.00 and LibGDX (full version) version 0.92. Any help you could give regarding this would be really great. Thanks, Rob

Just tried some of the Examples and it is doing the same thing.
 
Last edited:

srspinho

Member
Licensed User
Longtime User
Hi friend,

It should be a silly question, but I am trying to run the source code sample on my B4A 3.5 and I am viewing always the same error :

Parsing code. 0.00
Compiling code. Error
Error compiling program.
Error description: Too many parameters.
Occurred on line: 38
surface = lGdx.InitializeView(True, "LG")
Word: LG

I know there are something missing here, but I can not fid what. Is it necessary to download and instal the OpenGl ES 2.0 ? or should I update some package in order to get it working ?

Thank you for your help.

best regards,

Sérgio
 

Informatix

Expert
Licensed User
Longtime User
Hi friend,

It should be a silly question, but I am trying to run the source code sample on my B4A 3.5 and I am viewing always the same error :

Parsing code. 0.00
Compiling code. Error
Error compiling program.
Error description: Too many parameters.
Occurred on line: 38
surface = lGdx.InitializeView(True, "LG")
Word: LG

I know there are something missing here, but I can not fid what. Is it necessary to download and instal the OpenGl ES 2.0 ? or should I update some package in order to get it working ?

Thank you for your help.

best regards,

Sérgio
I updated the libGDX lib recently and removed the support of OpenGL ES 1.x so the parameter for OpenGL 2 is no longer needed. You can remove the parameter.
 

srspinho

Member
Licensed User
Longtime User
Hi Friend,

I´m still trying to run the source on my B4a Ver. 3.50

I decided to go one step back and test the libGDX samples.

Well, your tip about removing the parameter worked.

But, when I try to deploy it on the emulator, it does not start, showing a Java.lang error :

B4X:
LogCat connected to: emulator-5554
Copying updated assets files (1)
** Activity (main) Create, isFirst = true **
Error occurred on line: 38 (main)
java.lang.RuntimeException: Libgdx requires OpenGL ES 2.0
    at com.badlogic.gdx.backends.android.AndroidGraphics.createGLSurfaceView(AndroidGraphics.java:124)
    at com.badlogic.gdx.backends.android.AndroidGraphics.<init>(AndroidGraphics.java:96)
    at com.badlogic.gdx.backends.android.AndroidApplication.initializeForView(AndroidApplication.java:292)
    at com.badlogic.gdx.backends.android.AndroidApplication.initializeForView(AndroidApplication.java:273)
    at anywheresoftware.b4a.libgdx.LibGDX.InitializeView(LibGDX.java:58)
    at com.easyandroidcoding.cloneybirdpart1.main._activity_create(main.java:313)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:521)
    at anywheresoftware.b4a.shell.Shell.runMethod(Shell.java:636)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:302)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:238)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:521)
    at anywheresoftware.b4a.ShellBA.raiseEvent2(ShellBA.java:121)
    at com.easyandroidcoding.cloneybirdpart1.main.afterFirstLayout(main.java:98)
    at com.easyandroidcoding.cloneybirdpart1.main.access$100(main.java:16)
    at com.easyandroidcoding.cloneybirdpart1.main$WaitForLayout.run(main.java:76)
    at android.os.Handler.handleCallback(Handler.java:587)
    at android.os.Handler.dispatchMessage(Handler.java:92)
    at android.os.Looper.loop(Looper.java:123)
    at android.app.ActivityThread.main(ActivityThread.java:4627)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:521)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
    at dalvik.system.NativeStart.main(Native Method)
** Activity (main) Resume **

Deploy_Error.png



I´m using the default emulator on the B4A :

Emulator.png


The error appears when the applicatio try to start.

I tryed to enable the GPU emulation, (see bellow) but it does not fixed the error.

Config.png


After looking for the solution on Google, without any success, I tryed to compile and execute the PacDroid source code.

But, when I try to do that, the compiler returns an error :

"Error Description: Unknown member: usegl20"

PacDroid_Compile_Error.png


I have installed the libs : LibGDX (version: 0.96), OpenGL and OpenGL2 (version: 1.40)

SHould I install an extra package with OpenGL ES 2.0 ?

If yes, could you, please, recomend a link where I can download it ?

I´m using WIndows 7 64 bits

Thank you very much !

My best regards,

Sérgio Pinheiro
 

Informatix

Expert
Licensed User
Longtime User
Hi Friend,

I´m still trying to run the source on my B4a Ver. 3.50

I decided to go one step back and test the libGDX samples.

Well, your tip about removing the parameter worked.

But, when I try to deploy it on the emulator, it does not start, showing a Java.lang error :

B4X:
LogCat connected to: emulator-5554
Copying updated assets files (1)
** Activity (main) Create, isFirst = true **
Error occurred on line: 38 (main)
java.lang.RuntimeException: Libgdx requires OpenGL ES 2.0
    at com.badlogic.gdx.backends.android.AndroidGraphics.createGLSurfaceView(AndroidGraphics.java:124)
    at com.badlogic.gdx.backends.android.AndroidGraphics.<init>(AndroidGraphics.java:96)
    at com.badlogic.gdx.backends.android.AndroidApplication.initializeForView(AndroidApplication.java:292)
    at com.badlogic.gdx.backends.android.AndroidApplication.initializeForView(AndroidApplication.java:273)
    at anywheresoftware.b4a.libgdx.LibGDX.InitializeView(LibGDX.java:58)
    at com.easyandroidcoding.cloneybirdpart1.main._activity_create(main.java:313)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:521)
    at anywheresoftware.b4a.shell.Shell.runMethod(Shell.java:636)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:302)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:238)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:521)
    at anywheresoftware.b4a.ShellBA.raiseEvent2(ShellBA.java:121)
    at com.easyandroidcoding.cloneybirdpart1.main.afterFirstLayout(main.java:98)
    at com.easyandroidcoding.cloneybirdpart1.main.access$100(main.java:16)
    at com.easyandroidcoding.cloneybirdpart1.main$WaitForLayout.run(main.java:76)
    at android.os.Handler.handleCallback(Handler.java:587)
    at android.os.Handler.dispatchMessage(Handler.java:92)
    at android.os.Looper.loop(Looper.java:123)
    at android.app.ActivityThread.main(ActivityThread.java:4627)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:521)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
    at dalvik.system.NativeStart.main(Native Method)
** Activity (main) Resume **

View attachment 23901


I´m using the default emulator on the B4A :

View attachment 23899

The error appears when the applicatio try to start.

I tryed to enable the GPU emulation, (see bellow) but it does not fixed the error.

View attachment 23900

After looking for the solution on Google, without any success, I tryed to compile and execute the PacDroid source code.

But, when I try to do that, the compiler returns an error :

"Error Description: Unknown member: usegl20"

View attachment 23902

I have installed the libs : LibGDX (version: 0.96), OpenGL and OpenGL2 (version: 1.40)

SHould I install an extra package with OpenGL ES 2.0 ?

If yes, could you, please, recomend a link where I can download it ?

I´m using WIndows 7 64 bits

Thank you very much !

My best regards,

Sérgio Pinheiro
As the error message says: "Libgdx requires OpenGL ES 2.0"
If the device does not have OpenGL ES 2, that cannot work. It's not related to the OpenGL libraries for B4A, which are useless here.
 

srspinho

Member
Licensed User
Longtime User
Thanks !

I have installed the Genymotion and I can execute it.

I had to comment the line

Config.useGL20 = True

And change the line lGdx.Initialize2(Config)

to lGdx.Initialize2(Config, "LG")

The source compiles ok, installs and execute on the emulator. I just can't move the Pacman around the maze.

I´m still trying.

regards.
 

Beja

Expert
Licensed User
Longtime User
Thanks informatix,
This is a very good tutorial for game programming. the only thing I need now is the game idea!
God bless you.
 

srspinho

Member
Licensed User
Longtime User
Hello!
You're right
I read that just after my last message.
What a shame...

Thank you for your hwlp!
 

GMan

Well-Known Member
Licensed User
Longtime User
The new version works fine, the old one doesn't :)
 
Top