Introduction:
Pac-Man is an arcade game developed by Namco and licensed for distribution in the United States by Midway, in October 1980 and first released in Japan on May 22, 1980. Immensely popular from its original release to the present day, Pac-Man is considered one of the classics of the medium, virtually synonymous with video games, and an icon of 1980s popular culture Upon its release.
This article describes and explains a near replica of the classic pacman game, throughout the article different code sections are explained and the whole source code alongwith the executable is provided at the end of the article. The Code is written in Visual Studio 2012 using .Net 4.5 Framework. In order to compile and execute, the .Net Runtime 4.5 is required.
Basic WorkFlow Abstraction:
The Flow control undergoes a complex path during the execution of the game. but we’ll be looking at it in abstract way.
1) Present The Menu. 1.1) Load the Desired Map or Saved Game. 1.2) Load the Sprites and other files. 1.3) Build the game environment. 1.4) Play Intro music. 2) Start the Timer 2.1) Move the Pacman according to user input. 2.2) Move the Ghosts. 2.3) Check for collisions between the character types. 2.4) check for game over events. 3) Timer Call starts again and control jumps to location 2.
Obviously we have tried to keep it to the minimum detail, but don’t panic, the rest of the article presents all the nitty grifty details about the program.
Diving Right Into The Code:
When the game starts, the background of the menu is animated. It actually is the game engine running and the characters move around randomly in the default map. the area of map and the characters have a light grey overlay. The method responcible for this is InitializeIdleScreen.
public void InitializeIdleScreen() { this.IntroPlaying = false; this.SpriteShifter.Enabled = false; this.IntroMusicTimer.Enabled = false; this.FruitPlacementTimer.Enabled = false; Program.OverLayEnabled = true; LoadMapToArray("defaultmap.map"); InitializeCharacters(); ReallocateAllSprites(); OverLayAllSprites(); this.BackGroundImage = new Bitmap(640, 504); BackImageGfx = Graphics.FromImage(BackGroundImage); BuildBackGroundImage(); this.BackgroundImage = this.BackGroundImage; this.Controls.Clear(); this.Controls.Add(LogoBox); this.MainPanel.Enabled = true; this.Controls.Add(MainPanel); this.SoundFxPlayer.Stream = global::PACMAN.Properties.Resources.Intro; this.PacManMoveDelegate = new NoParaNoReturn(BlankFun); Red.SetPathDelegate = new NoParaNoReturn(Red.SetRandomTargetPath); Red.CheckCollisionDelegate = new NoParaNoReturn(this.BlankFun); Blue.CheckCollisionDelegate = new NoParaNoReturn(this.BlankFun); Pink.CheckCollisionDelegate = new NoParaNoReturn(this.BlankFun); Yellow.CheckCollisionDelegate = new NoParaNoReturn(this.BlankFun); FirstPac.ClearBackImageDelegate = new TwoIntParaNoReturn(FirstPac.ClearBackGroundImageOneBlockOverLayed); this.KeyPreview = false; if(Program.HighPerformance==true) FormRefreshDelegate = new NoParaNoReturn(Refresh); else FormRefreshDelegate = new NoParaNoReturn(Invalidate); this.Mover.Enabled = true; }
As seen in the code, the method first does some initial work, then loads the default map into a 2D array of characters (because the ghost characters roam around in defaultmap in idle screen). then all the characters are initialized according to the map and sprites are assigned (sprites are sequences of images that show a moving or animating object). it then overlays a grey color over all the sprites. then the background image is built (the map is drawn as an image in memory rather than being re-drawn in each Paint() call. Further in the method some more initialization is done which will become clear in the upcoming sections.
The code above called a method BuildBackGroundImage, so here we take a look at what it does actually.
public void BuildBackGroundImage() { Image PalletImg = global::PACMAN.Properties.Resources.pallet; Image PowerPalletImg = global::PACMAN.Properties.Resources.powerpallet; SolidBrush b = new SolidBrush(Color.FromArgb(0, 51, 255)); Pen p = new Pen(b, 5); for (int row = 0; row < 24; row++) { for (int col = 0; col < 32; col++) { if (ExistsInMap(row, col)) { bool drawn = false; if (ExistsInMap(row, col + 1)) { if (ExistsInMap(row + 1, col)) { this.BackImageGfx.DrawArc(p, (20 * col) + 7, (20 * row) + 5, 15, 25, 180, 100); drawn = true; } if (ExistsInMap(row - 1, col)) { this.BackImageGfx.DrawArc(p, (20 * col) + 7, (20 * row) - 20, 25, 25, 90, 90); drawn = true; } } else if (ExistsInMap(row, col - 1)) { if (ExistsInMap(row + 1, col)) { this.BackImageGfx.DrawArc(p, (20 * col) - 8, (20 * row) + 5, 15, 22, 270, 100); drawn = true; } if (ExistsInMap(row - 1, col)) { this.BackImageGfx.DrawArc(p, (20 * col) - 18, (20 * row) - 20, 25, 25, 360, 90); drawn = true; } } if (!drawn) { if (ExistsInMap(row, col + 1) || ExistsInMap(row, col - 1)) this.BackImageGfx.DrawLine(p, 20 * col - 4, (20 * row) + 5, (20 * col) + 20, (20 * row) + 5); else if (ExistsInMap(row + 1, col) || ExistsInMap(row - 1, col)) this.BackImageGfx.DrawLine(p, (20 * col) + 7, (20 * row) - 7, (20 * col) + 7, (20 * row) + 18); } } else if (Map[row, col] == '|') { p.Brush = new SolidBrush(Color.FromArgb(255, 189, 228)); if (Map[row, col - 1] == '|' || Map[row, col + 1] == '|') BackImageGfx.DrawLine(p, 20 * col - 4, (20 * row) + 5, (20 * col) + 20, (20 * row) + 5); else if (Map[row + 1, col] == '|' || Map[row - 1, col] == '|') BackImageGfx.DrawLine(p, (20 * col) + 7, (20 * row) - 7, (20 * col) + 7, (20 * row) + 18); p.Brush = b; } else if (Map[row, col] == ' ') BackImageGfx.DrawImage(PalletImg, new Point((20 * col) - 5, (20 * row) - 6)); else if (Map[row, col] == 'V') BackImageGfx.DrawImage(PowerPalletImg, new Point((20 * col) - 5, (20 * row) - 6)); } } if(Program.OverLayEnabled==true) BackImageGfx.FillRectangle(new SolidBrush(OverLayColor), new Rectangle(new Point(0, 0), BackGroundImage.Size)); }
The method basically reads the map from the 2D array of characters and draws accordingly to a Bitmap. That Bitmap is used in game as the background image. Another way to do this could have been to put all these lines of code in the Paint() event. but that would have eaten up a major chunk of performance. (Drawing an image to screen takes lesser performace than evaluating 1900+ conditions and drawing to screen).
The constructor of the GameForm class does some basic initialization job and is not explained in this article, but these three lines from the constructor worth mentioning them.
SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.DoubleBuffer, true);
These lines of code actually enable Double Buffering. Double Buffering is necessary for games that require smooth visual experience. Moving something on the screen requires first clearing it and then reprinting it on the new location, between the process there comes a moment when the screen is blank, which is commonly reffered to as flicker, Double buffering tries to solve the issue by managing two video memory locations, The display device reads the data from one location while the program draws to other, when the drawing process is complete the display device is directed to read from the second location. and vice versa.
Another important method is the FindPath method that helps Ghosts find their path to their destination.
public static List FindPath(Point StartPoint, Point DestinationPoint) { BlockMap = new BlockOnMap[24, 32]; BlockOnMap StartBlock = new BlockOnMap(StartPoint, null, BlockStatus.Open, 0, 0); BlockOnMap DestinationBlock; BlockOnMap SelectedBlock = StartBlock; List SelectedPath = new List(); ///////////Initialize Block Map////////////// for (int row = 0; row < 24; row++) for (int col = 0; col < 32; col++) BlockMap[row, col] = new BlockOnMap(new Point(col, row), null, Program.GameWindow.Map[row, col] == '#' ? BlockStatus.Wall : Program.GameWindow.Map[row, col] == '|' ? BlockStatus.Wall : BlockStatus.Unexplored, 0, 0); BlockMap[StartPoint.Y, StartPoint.X] = StartBlock; DestinationBlock = BlockMap[DestinationPoint.Y, DestinationPoint.X]; //////////////////////////////////////////// while (true) { SelectedBlock.Status=BlockStatus.Closed; if (DestinationBlock.Status==BlockStatus.Closed) break; List SurroundingBlocks=GetSurrounding(SelectedBlock); foreach(BlockOnMap adjacentblock in SurroundingBlocks) { if (adjacentblock.Status == BlockStatus.Open) { if (adjacentblock.G+1 < SelectedBlock.G) { SelectedBlock.Parent = adjacentblock; SetFGH(SelectedBlock, DestinationBlock); } } else { adjacentblock.Status = BlockStatus.Open; adjacentblock.Parent = SelectedBlock; SetFGH(adjacentblock, DestinationBlock); } } SurroundingBlocks = (from n in SurroundingBlocks where n.Status == BlockStatus.Open orderby n.F select n).ToList()/*[0]*/; if (SurroundingBlocks.Count == 0) break; else SelectedBlock = SurroundingBlocks[0]; } BlockOnMap pathblock = DestinationBlock; while (pathblock != null) { SelectedPath.Add(pathblock.LocationOnActualMap); pathblock = pathblock.Parent; } return SelectedPath; }
The method takes 2 points as paramters and uses the A* Search Algorithm to find the shortest (in most cases) path from StartPoint to EndPoint. Not All gohsts chase pacman, only the Red ghost actually chases pacman, rest of them just travel to random locations in map.
Conclusion:
The article described the important parts of source code, the whole of it could take days to write (and possibly exceed the max length of the post set by the admin of blog). The game is developed using GDI+, which wasn’t really meant for games. so the game eats up a major chunk of CPU time.
the source code can be downloaded from here while the game executable can be downloaded from here.