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)
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 SubIViewEventsHandler:
'@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 SubWorksheetViewWrapper
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 FunctionAdapter
'@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 SubController
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 SubClient - 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 SubI 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
- Everyone's favorite VBE addin,Rubberduck!
- This SO answer which got me thinking about all this VBA OOP viability in the first place.
- This excel version of Battleship from which I have mimicked the Adapter-events-passing pattern.
- Pacman Dossier has very detailed analysis of the inner workings of pacman.
- 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\$Mathieu Guindon– Mathieu Guindon2020-09-01 23:13:58 +00:00CommentedSep 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\$RubberDuck– RubberDuck2020-09-02 00:01:05 +00:00CommentedSep 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\$ArcherBird– ArcherBird2020-09-02 00:31:48 +00:00CommentedSep 2, 2020 at 0:31
- 2\$\begingroup\$You're off to a great start. Have you readUnderstanding Pac-Man Ghost Behavior?\$\endgroup\$TinMan– TinMan2020-09-02 04:04:20 +00:00CommentedSep 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\$ArcherBird– ArcherBird2020-09-02 12:58:15 +00:00CommentedSep 2, 2020 at 12:58
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.



