30
\$\begingroup\$

What?, Why?

I have been inspired by several other posts on the topic of OOP implementations in VBA to try and create a Pacman clone. I think this task is not all that hard in most languages; but, I first learned how to code via VBA and I, perhaps, have a masochistic fondness for the framework. VBA comes with a host of challenges (lack of inheritance, single thread environment, etc.) that I seek to overcome.

One of my main goals with an OOP implementation is to have the game logic decoupled from the UI so that one could implement a UI as an Excel Worksheet, or a Userform, or whatever else you might imagine available to you in VBA. The other goal is to get as close to the real game rules as I can.

There will be a lot to go over, so I hope you don't mind me breaking this into multiple code review posts, each with some smaller scope of focus. For this post, I'd like to get feedback on my overall architecture plan, and give you a very general look at the game logic class and how I intend to make it interface-able to a UI. (The first UI implementation will be an Excel Worksheet).

Architecture

The design will resemble an MVP pattern with the idea that the View part of the architecture can be implemented in any number of ways. Again, in this case, I will implement an Excel Worksheet-based view for the game. I've included a (likely incomplete) diagram to help illustrate as well as my Rubberduck folder structure (also incomplete)

MVC diagram of classes

Rubbberduck folder structure

Models
Models will consist of the various game elements. In a truly decoupled design, these would be simplePOVOs(?), but I plan to encapsulate some game logic into the models because these models won't ever have much use outside of the context of the Pacman game and it will make the game controller a little simpler. For example, characters like pacman and the ghosts will know how to move around in the maze. That way, the controller can simply call aMove() member in each of them. I will save the models' code for another code review post.

View
I don't really care too much about having a super flexible, independent view; so in my design, any view implemented for the Pacman game will know about the models. This will make passing data to the view much simpler because we can just pass the entire model(s). The game controller will talk to the view through an interface layer. The idea is that the View will implement theIGameEventsHandler interface so that the Controller can call methods in the View as game events happen. The View will also have a reference to anIViewEventsHandler class so that it can call event methods to notify the Controller of user generated events.

Controller
The controller will hold much of the game logic and will facilitate the continuous ticking of the game progression. Because of how events are done in VBA, I have an extraViewAdapter class that will help facilitate listening to events from the View. When user generated things happen, the View can callIViewEventsHandler methods in the concreteViewAdapter class, which will, in turn, raise an event to the controller. This way, events from the View can "interrupt" the game ticking in the controller thanks toDoEvents calls that will happen with every tick. (This is step 1 in overcoming our single-thread limitation).

Code Samples

Interfaces

IGameEventsHandler:

'@Folder "PacmanGame.View"'@Interface'@ModuleDescription("Methods that the Controller will need to be able to call in the UI. These are things the Controller will need to tell the UI to do.")Option ExplicitPrivate Const mModuleName As String = "IGameEventsHandler"'@Description("Provides a way for the ViewAdapter to hook itself into an  IGameEventsHandler implementer")Public Property Get Events() As IViewEventsHandlerEnd PropertyPublic Property Set Events(ByVal value As IViewEventsHandler)End PropertyPublic Sub CreateMap(map() As Tile)End SubPublic Sub CreatePacman(character As PacmanModel)End SubPublic Sub CreateGhost(character As GhostModel)End SubPublic Sub UpdateComponents(gamePieces As Collection)End SubPrivate Sub Class_Initialize()    Err.Raise 5, mModuleName, "Interface class must not be instantiated."End Sub

IViewEventsHandler:

'@Folder "PacmanGame.View"'@Interface'@ModuleDescription("Methods that the UI can call to notify the controller of user interaction. These are events from the UI that the Controller wants to hear about")Option ExplicitPrivate Const mModuleName As String = "IViewEventsHandler"Public Enum KeyCode    LeftArrow = 37    RightArrow = 39    UpArrow = 38    DownArrow = 40End EnumPublic Sub OnDirectionalKeyPress(vbKey As KeyCode)End SubPublic Sub OnGameStarted()End SubPublic Sub OnGamePaused()End SubPublic Sub OnQuit()End SubPrivate Sub Class_Initialize()    Err.Raise 5, mModuleName, "Interface class must not be instantiated."End Sub

WorksheetViewWrapper

This is a facade class that will wrap anExcel.Worksheet to use as our UI. This codecould go directly into a worksheet class, but alas, you cannot make a worksheetImplement anything in the code-behind.

'@Folder "ViewImplementations.ExcelWorksheet"'//UI implemented as an Excel WorksheetOption ExplicitImplements IGameEventsHandlerPrivate Const MAP_START_ADDRESS As String = "$D$3"Private Type TWorksheetViewWrapper    MapRange As Range    dPad As Range    Adapter As IViewEventsHandler    ShapeWrappers As Dictionary    YIndexOffset As Long    XIndexOffset As LongEnd TypePrivate WithEvents innerWs As WorksheetPrivate this As TWorksheetViewWrapperPublic Sub Init(xlWs As Worksheet)    Dim s As Shape        For Each s In xlWs.Shapes        s.Delete    Next        xlWs.Activate    xlWs.Range("AE65").Select    Set innerWs = xlWs    Set this.dPad = xlWs.Range("AE65")End SubPrivate Sub Class_Initialize()    Set this.ShapeWrappers = New DictionaryEnd SubPrivate Sub Class_Terminate()    Set this.Adapter = Nothing    Set innerWs = Nothing    Set this.dPad = Nothing     Debug.Print TypeName(Me) & " terminating..."End Sub'// Support for IGameEventsHandlerPrivate Sub IGameEventsHandler_CreateGhost(character As GhostModel)    '// Create a corrosponding ViewModel Ghost    Dim newGhostShape As New GhostStyler    newGhostShape.Init innerWs, character.Color    '// Add him to the drawing collection    this.ShapeWrappers.Add character.Name, newGhostShape    End SubPrivate Sub IGameEventsHandler_CreatePacman(character As PacmanModel)    '// Create a corrosponding ViewModel Pacman    Dim newPacmanShape As New PacmanStyler    newPacmanShape.Init innerWs        '// Add him to the drawing collection    this.ShapeWrappers.Add character.Name, newPacmanShape    End SubPrivate Sub IGameEventsHandler_CreateMap(map() As Tile)    this.YIndexOffset = 1 - LBound(map, 1)    this.XIndexOffset = 1 - LBound(map, 2)        Set this.MapRange = innerWs.Range(MAP_START_ADDRESS).Resize(UBound(map, 1) + this.YIndexOffset, UBound(map, 2) + this.XIndexOffset)End SubPrivate Sub IGameEventsHandler_UpdateComponents(characters As Collection)    Dim character As IGamePiece    Dim characterShape As IDrawable    Dim i As Integer        For Each character In characters        '// use the id from each character to get the corresponding ShapeWrapper        Set characterShape = this.ShapeWrappers.Item(character.Id)        characterShape.Redraw character.CurrentHeading, TileToRange(character.CurrentTile)            NextEnd SubPrivate Property Set IGameEventsHandler_Events(ByVal RHS As IViewEventsHandler)    Set this.Adapter = RHSEnd PropertyPrivate Property Get IGameEventsHandler_Events() As IViewEventsHandler    Set IGameEventsHandler_Events = this.AdapterEnd Property'// Events from the worksheet that we will translate into view eventsPrivate Sub innerWs_Activate()    '// maybe pause the game?End SubPrivate Sub innerWs_Deactivate()    '// maybe we need a resume game event?End SubPrivate Sub innerWs_SelectionChange(ByVal Target As Range)    If this.dPad.Offset(-1, 0).Address = Target.Address Then        this.Adapter.OnDirectionalKeyPress UpArrow    ElseIf this.dPad.Offset(1, 0).Address = Target.Address Then        this.Adapter.OnDirectionalKeyPress (DownArrow)    ElseIf this.dPad.Offset(0, -1).Address = Target.Address Then        this.Adapter.OnDirectionalKeyPress (LeftArrow)    ElseIf this.dPad.Offset(0, 1).Address = Target.Address Then        this.Adapter.OnDirectionalKeyPress (RightArrow)    End If        Application.EnableEvents = False    this.dPad.Select    Application.EnableEvents = TrueEnd Sub'// Private helpersPrivate Function TileToRange(mapTile As Tile) As Range    Set TileToRange = this.MapRange.Cells(mapTile.y + this.YIndexOffset, mapTile.x + this.XIndexOffset)End Function

Adapter

'@Folder "PacmanGame.View"Option ExplicitImplements IViewEventsHandlerImplements IGameEventsHandlerPrivate Const mModuleName As String = "ViewAdapter"Private viewUI As IGameEventsHandlerPublic Event DirectionalKeyPressed(vbKeyCode As KeyCode)Public Event GameStarted()Public Event GamePaused()Public Event Quit()Public Sub Init(inViewUI As IGameEventsHandler)    Set viewUI = inViewUI    Set viewUI.Events = MeEnd SubPublic Sub Deconstruct()    '// unhooks itself from the GameEventsHandler to prevent memory leakage    Set viewUI.Events = NothingEnd SubPublic Function AsCommandSender() As IGameEventsHandler    '// allows access to the IGameEventsHandler methods    Set AsCommandSender = MeEnd FunctionPrivate Sub Class_Terminate()    Set viewUI = Nothing    Debug.Print TypeName(Me) & " terminating..."End Sub'//IGameEventsHandler SupportPrivate Property Set IGameEventsHandler_Events(ByVal RHS As IViewEventsHandler)    '//this isn't meant to be set from the outside for this classEnd PropertyPrivate Property Get IGameEventsHandler_Events() As IViewEventsHandler    Set IGameEventsHandler_Events = MeEnd PropertyPrivate Sub IGameEventsHandler_CreateGhost(character As GhostModel)    viewUI.CreateGhost characterEnd SubPrivate Sub IGameEventsHandler_CreatePacman(character As PacmanModel)    viewUI.CreatePacman characterEnd SubPrivate Sub IGameEventsHandler_CreateMap(map() As Tile)    viewUI.CreateMap mapEnd SubPrivate Sub IGameEventsHandler_UpdateComponents(characters As Collection)    viewUI.UpdateComponents charactersEnd Sub'//IViewEventsHandler SupportPrivate Sub IViewEventsHandler_OnDirectionalKeyPress(vbKey As KeyCode)    RaiseEvent DirectionalKeyPressed(vbKey)End SubPrivate Sub IViewEventsHandler_OnGamePaused()    RaiseEvent GamePausedEnd SubPrivate Sub IViewEventsHandler_OnGameStarted()    RaiseEvent GameStartedEnd SubPrivate Sub IViewEventsHandler_OnQuit()    RaiseEvent QuitEnd Sub

Controller

This class is obviously a WIP, but I've included it here to show how the Controller uses a ViewAdapter to send/receive messages to/from the View

'@Folder "PacmanGame.Controller"'@ExposedOption ExplicitPrivate Const mModuleName As String = "GameController"Private Const SECONDS_PER_TICK As Double = 0.06 '// sets a minimum amount of time (in seconds) that will pass between game ticksPrivate Const TICK_CYCLE_RESOLUTION As Double = 10 '// helps faciliate game pieces moving at different speedsPublic WithEvents UIAdapter As ViewAdapterPublic Enum Direction    dNone = 0    dUp = -1    dDown = 1    dLeft = -2    dRight = 2End Enum'//Encasulated FieldsPrivate Type TGameController    IsGameOver As Boolean    Maze() As Tile    TickCounter As Long    Ghosts As Collection    GamePieces As Collection    Player As PacmanModelEnd TypePrivate this As TGameControllerPublic Sub StartGame()    '// this is here to temporarily provide a way for me to kick off the game from code    UIAdapter_GameStartedEnd SubPrivate Sub Class_Initialize()    Set this.GamePieces = New CollectionEnd SubPrivate Sub Class_Terminate()    Debug.Print TypeName(Me) & " terminating..."    Set this.GamePieces = Nothing        UIAdapter.Deconstruct        Erase this.Maze    Erase MapManager.Maze    Set UIAdapter = NothingEnd Sub'// This is the main engine of the game that is called repeatedly until the game is overPrivate Sub Tick()    Dim t As Double        t = Timer        Dim character As IGamePiece        For Each character In this.GamePieces            If character.CycleRemainder >= TICK_CYCLE_RESOLUTION Then            character.CycleRemainder = character.CycleRemainder Mod TICK_CYCLE_RESOLUTION            character.Move                            Else            If this.TickCounter Mod Round(TICK_CYCLE_RESOLUTION / (TICK_CYCLE_RESOLUTION * (1 - character.Speed)), 0) <> 0 Then               character.CycleRemainder = character.CycleRemainder + TICK_CYCLE_RESOLUTION Mod (TICK_CYCLE_RESOLUTION * (1 - character.Speed))               character.Move            End If                        If Round(TICK_CYCLE_RESOLUTION / (TICK_CYCLE_RESOLUTION * (1 - character.Speed)), 0) = 1 Then               character.CycleRemainder = character.CycleRemainder + TICK_CYCLE_RESOLUTION Mod (TICK_CYCLE_RESOLUTION * (1 - character.Speed))            End If                    End If    Next        '// TODO: check if player died and/or there is a game over... account for player Lives > 1    'If this.Player.IsDead Then IsGameOver = True        '// update the view    UIAdapter.AsCommandSender.UpdateComponents this.GamePieces            '// ensure a minimum amount of time has passed    Do    DoEvents    Loop Until Timer > t + SECONDS_PER_TICKEnd Sub'//ViewEvents HandlingPrivate Sub UIAdapter_DirectionalKeyPressed(vbKeyCode As KeyCode)    Select Case vbKeyCode        Case KeyCode.UpArrow            this.Player.Heading = dUp        Case KeyCode.DownArrow            this.Player.Heading = dDown        Case KeyCode.LeftArrow            this.Player.Heading = dLeft        Case KeyCode.RightArrow            this.Player.Heading = dRight    End SelectEnd SubPrivate Sub UIAdapter_GameStarted()'// TODO: unbloat this a bit!    '// initialize vars        '//scoreboard        '//            '// initialize game peices    Dim blinky As GhostModel    Dim inky As GhostModel    Dim pinky As GhostModel    Dim clyde As GhostModel        '// set up maze    this.Maze = MapManager.LoadMapFromFile    MapManager.Maze = this.Maze    UIAdapter.AsCommandSender.CreateMap this.Maze        '// set up pacman    Set this.Player = New PacmanModel    Set this.Player.CurrentTile = MapManager.GetMazeTile(46, 30)    this.GamePieces.Add this.Player    UIAdapter.AsCommandSender.CreatePacman this.Player        '// set up ghosts    Set blinky = BuildGhost("Blinky", vbRed, MapManager.GetMazeTile(22, 30), ShadowBehavior.Create(this.Player))    this.GamePieces.Add blinky    UIAdapter.AsCommandSender.CreateGhost blinky        Set pinky = BuildGhost("Pinky", rgbLightPink, MapManager.GetMazeTile(22, 20), SpeedyBehavior.Create(this.Player))    this.GamePieces.Add pinky    UIAdapter.AsCommandSender.CreateGhost pinky        Set inky = BuildGhost("Inky", vbCyan, MapManager.GetMazeTile(22, 34), BashfulBehavior.Create(this.Player, blinky))    this.GamePieces.Add inky    UIAdapter.AsCommandSender.CreateGhost inky        Set clyde = BuildGhost("Clyde", rgbOrange, MapManager.GetMazeTile(22, 37), RandomBehavior.Create())    this.GamePieces.Add clyde    UIAdapter.AsCommandSender.CreateGhost clyde        '//play intro            this.TickCounter = 0        Do While Not this.IsGameOver            'DoEvents        'If  TickCounter = MaxCycles Then  TickCounter = 0        this.TickCounter = this.TickCounter + 1        Tick        'DoEvents    Loop        End Sub'//Private HelpersPrivate Function BuildGhost(Name As String, _                            Color As Long, _                            startTile As Tile, behavior As IGhostBehavior) As GhostModel    Dim newGhost As GhostModel    Set newGhost = New GhostModel        With newGhost        .Name = Name        .Color = Color        Set .CurrentTile = startTile        Set .ActiveBehavior = behavior    End With        Set BuildGhost = newGhostEnd FunctionPrivate Sub BuildGameBoard()    UIAdapter.AsCommandSender.CreateMap Me.MazeEnd Sub

Client - putting it all together:

Here is some sample code that illustrates how some client code might snap all the pieces together to have a functioning game.

Public Sub Main()    '//get our concrete sheet    Dim xlWs As Worksheet    Set xlWs = Sheet1        '//wrap it up    Dim sheetWrapper As WorksheetViewWrapper    Set sheetWrapper = New WorksheetViewWrapper    sheetWrapper.Init xlWs    '//give it to a game adapter    Dim viewUIAdapter As ViewAdapter    Set viewUIAdapter = New ViewAdapter    viewUIAdapter.Init sheetWrapper        '//hand that to a new controller    Set mController = New GameController    Set mController.UIAdapter = viewUIAdapter    '//start the game!    mController.StartGameEnd Sub

I welcome any critiques on my architecture plan, naming conventions, and even nit picks! One of my specific questions is this: at some point I need to configure the game controller by setting its player, viewAdapter, ghosts, map, etc. properties. It seems to me that the ViewAdapter should be injected from the outside. Should the other components also be injected? or should I just let the controller configure all of these internally?

I have published my entire project to agithub repo so that you can build and run what I have working so far. There are so many parts in this project, so please forgive me as I attempt to balancecompleteness withoverbroadness in my posts. In forthcoming posts, I plan to ask for code review on these topics: Moving game piece models and moving them at different speeds, map/maze building and interaction, animating game action in the view, and probably some others as I further development. Thank you for reading this whole thing!!!

Acknowledgements

  1. Everyone's favorite VBE addin,Rubberduck!
  2. This SO answer which got me thinking about all this VBA OOP viability in the first place.
  3. This excel version of Battleship from which I have mimicked the Adapter-events-passing pattern.
  4. Pacman Dossier has very detailed analysis of the inner workings of pacman.
Sᴀᴍ Onᴇᴌᴀ's user avatar
Sᴀᴍ Onᴇᴌᴀ
29.6k16 gold badges46 silver badges203 bronze badges
askedSep 1, 2020 at 21:14
ArcherBird's user avatar
\$\endgroup\$
7
  • 7
    \$\begingroup\$"[...] and I, perhaps, have a masochistic fondness for the framework. VBA comes with a host of challenges [...] that I seek to overcome." - this, right there. So much this! #NotAlone ;-) ...would love to see this on GitHub!\$\endgroup\$CommentedSep 1, 2020 at 23:13
  • 5
    \$\begingroup\$This is awesome. Not only is it Pac-Man, you included an architecture diagram.Chef’s Kiss\$\endgroup\$CommentedSep 2, 2020 at 0:01
  • 2
    \$\begingroup\$@RubberDuck thank you for the love! I'm glad to not be the only one enthusiastic about something as silly as this! <3\$\endgroup\$CommentedSep 2, 2020 at 0:31
  • 2
    \$\begingroup\$You're off to a great start. Have you readUnderstanding Pac-Man Ghost Behavior?\$\endgroup\$CommentedSep 2, 2020 at 4:04
  • 1
    \$\begingroup\$@MathieuGuindon repo added! I'm curious to see how well it will work on a machine other than my own!\$\endgroup\$CommentedSep 2, 2020 at 12:58

0

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.