Android Tutorial PacDroid: from scratch to fun

Discussion in 'Tutorials & Examples' started by Informatix, Sep 15, 2013.

  1. Informatix

    Informatix Expert Licensed User

    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:
    [​IMG]

    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:
    Code:
    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:
    Code:
    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:
    Code:
    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:
    Code:
    Sub LG_Render
          
    'Clears the screen
          GL.glClearColor(0001)
          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:
    Code:
    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:
    Code:
    argRightAnim = Atlas.FindRegions2("right")
    argDownAnim = Atlas.FindRegions2(
    "down")
    For i = 0 To 1
          argLeftAnim(i).Initialize(argRightAnim(i))
          argLeftAnim(i).Flip(
    TrueFalse)
          argUpAnim(i).Initialize(argDownAnim(i))
          argUpAnim(i).Flip(
    FalseTrue)
    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.
    Code:
    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:
    Code:
    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:
    Code:
    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:
    Code:
    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:
    Code:
    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:
    Code:
    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.
    Code:
    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: Mar 30, 2014
    stanks, JakeBullet70, gmilias and 9 others like this.
  2. Informatix

    Informatix Expert Licensed User

    PacDroid is alive!

    Getting the input from the accelerometer is very easy:
    Code:
    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:
    Code:
    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:
    Code:
    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:
    Code:
    Public Sub CanMoveTo(Tile As lgMapStaticTiledMapTileAs 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):
    Code:
    If Anim.GetKeyFrame(0) <> argUpAnim(0Then
          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:
    Code:
    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:
    Code:
    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:
    Code:
    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):
    Code:
    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:
    Code:
    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):
    Code:
    'Is the current move finished?
    If ContinueMove Then Return

    'Creates an array of possible moves
    Dim Moves(4As 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:
    Code:
    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:
    Code:
    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.51), Actions.RemoveActor))
    grpMain.AddActor(lblExtraPoints(CollidGhost))
    I use also actions for the title and for the PacDroid death:
    Code:
    PacDroid.Actor.AddAction(Actions.Sequence2( _
                             Actions.ScaleTo2(
    001), 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:
    Code:
    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:
    Code:
    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:
    Code:
    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.
     

    Attached Files:

    Last edited: Jul 4, 2014
  3. Theera

    Theera Well-Known Member Licensed User

    Awesome,I love it.
     
  4. Peter Simpson

    Peter Simpson Expert Licensed User

  5. Tom Christman

    Tom Christman Active Member Licensed User

    Thanks for all your work on the library, information and tutorial.....truly generous.
     
  6. marroh

    marroh New Member Licensed User

    Thank you mutch, love it! :)
     
  7. robbies

    robbies New Member Licensed 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: Oct 22, 2013
  8. Erel

    Erel Administrator Staff Member Licensed User

    You should test it with in "Release" mode. The debuggers doesn't handle this library as it raises events from background threads directly.
     
    robbies likes this.
  9. robbies

    robbies New Member Licensed User

    Brilliant! Thanks Erel :)
     
  10. pluton

    pluton Active Member Licensed User

    Wauuuu
    This is the most detailed tutorial I have read. Nice :D
     
  11. srspinho

    srspinho Member Licensed 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
     
  12. Informatix

    Informatix Expert Licensed User

    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.
     
  13. srspinho

    srspinho Member Licensed User

    Thank you !
     
  14. srspinho

    srspinho Member Licensed 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 :

    Code:
    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
     
  15. Informatix

    Informatix Expert Licensed User

    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.
     
  16. srspinho

    srspinho Member Licensed 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.
     
  17. Beja

    Beja Expert Licensed 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.
     
  18. Informatix

    Informatix Expert Licensed User

    If you read the first paragraph of the first chapter of this tutorial, you will see that the game uses the accelerometer.
     
  19. srspinho

    srspinho Member Licensed User

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

    Thank you for your hwlp!
     
  20. GMan

    GMan Well-Known Member Licensed User

    The new version works fine, the old one doesn't :)
     
Loading...
  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice