Flappy game version 3

object-oriented with own classes 

class, public, return, new, double, int, Canvas (Image, Rect, RectMode, Text, Width, Height, Timer, KeyIsPressed, KeyCode), CanvasElement (Move, Resize, Width, Height), System.IO.File (Exists), string (Replace), int (ToString)

As the last version, you should practice the high art of object-oriented programming by programming the Flappy game with your own classes. Altogether there are currently three important objects that can be considered as classes, the background, Flappy itself and the pillars.
But you can also program the game itself as a separate class and then “install” the Draw method there. You can start with the Game class, which should control the game, and with the Background class, for drawing the background (see figure).

Only the public methods that "expose" the two classes to the outside are listed in the figure. The actual program consists of creating an instance of the Game class and calling the Play method, which takes control of the game.

	Game game = new Game();
	game.Play();
As you already know from the last Flappy version 2, the Play method creates an instance of the Canvas class for the canvas and then sets the Timer variable to its own method, which you simply name Draw again.

	class Game
	{
		Canvas c;

		public void Play()
		{
			c = new Canvas();
			c.Timer = Draw;
		}

		void Draw()
		{
		}
	}
It is important that you declare the variable c within the class so that the Draw method can also access it. If you had created this in thePplay method, it would only be valid within this method.

Background

Your framework for the game is complete. Now it's time to program the background class. When creating the object with the new operator, this should be passed the canvas instance so that the Background class can draw on the canvas. There is a special method for passing values when creating the object, the so-called constructor method. This method is called automatically when an instance is created by new. This must have the same name as the class and must not have a return value. In this method, you can declare the variables that you want to transfer as parameters. In addition to the canvas instance, you also pass the file path to the background image to the constructor method. You then create the instance of the class in the Play method.

	class Game
	{
		Canvas c;
		Background background;

		public void Play()
		{
			c = new Canvas();
			c.Timer = Draw;
			background = new Background(c, @"C:\images\Background.png");
		}

		void Draw()
		{
		}
	}

	class Background
	{
		CanvasElement image;

		public Background(Canvas c, string path)
		{
			image = c.Image(path);
		}
	}
In the Game class, you also store the instance of the Background class within the Game class and not in the Play method, so that the Draw method later has access to the background variable.
In the constructor method of the Background class, the Image method of the Canvas instance is called. You also store the returned element within the Background class in a variable image so that you can use it in other methods that you are programming right away. When you start the program, the background image should already be displayed.
Now you can program the other methods of the background class. You should get or set the speed of the background using the GetSpeed and SetSpeed methods. Use GetHeight and GetWidth to determine the height and width of the image.

	class Background
	{
		CanvasElement image;
		double speed = 5;   
	...        
		public void SetSpeed(double newSpeed)
		{
			speed = newSpeed;
		}
		public double GetSpeed()
		{
			return speed;
		}
		public double GetHeight()
		{
			return image.Height;
		}
		public double GetWidth()
		{
			return image.Width / 2;
		}
	}
For the speed you create an instance variable speed and set it to 5. You determine the height using the size of the image. When it comes to the width, you have to remember that the image appears twice in a row, so you have to divide by 2.
The last method is to move the background from right to left using the Move method.

	class Background
	{
		CanvasElement image;
		double speed = 5;
		double x = 0;
	...
		public void Move()
		{
			x -= GetSpeed();

			image.Move(x, 0);

			if (x < -GetWidth())
				x = 0;
		}
	}
To do this, you declare an instance variable x that defines the x-position at which the background should appear horizontally. In the Move method, you subtract the x-coordinate by the value in speed. You then use the CanvasElement class's Move method to set the image to the new x-coordinate. Finally, you check whether the x-coordinate has already been moved far enough to the left for the image to be displayed completely and, if necessary, reset the x-coordinate to 0.
Nothing is moving in your program yet, since moving the background image still has to be "built in" in the game class. First set the height and width of the canvas in the Play method. Then add the call to the Move method of the Background class in the Draw method.

	class Game
	{
		Canvas c;
		Background background;

		public void Play()
		{
			c = new Canvas();
			c.Timer = Draw;

			background = new Background(c, @"C:\images\Background.png");

			c.Height = background.GetHeight();
			c.Width = background.GetWidth();    }

		void Draw()
		{
			background.Move();
		}
	}
You may only set the height and width after you have created the background object from the class. Otherwise the instance does not exist yet and you would get the error message "Object reference not set to an instance of an object".

Now comes Flappy

Next we want to program the Flappy class. Since the game is called Flappy, the class should be called Bird to distinguish it.

Like the Background class, the Bird class should first have a constructor method that is used to pass the canvas object and the file path to Flappy's image. The image should appear centered in the middle of the canvas. Since Flappy's x and y coordinates change, you create two instance variables here.

	class Bird
	{
		double x;
		double y;
		CanvasElement image;

		public Bird(Canvas c, string path)
		{
			x = c.Width / 2;
			y = c.Height / 2;

			image = c.Image(path, x, y);
		}
	}
You store the object returned by the Image method in an instance variable image so that you can move Flappy later. In order for Flappy to appear on the canvas, you must again create an object of the Bird class in the Play method of the Game class.

	class Game
	{
		Canvas c;
		Background background;
		Bird flappy;

		public void Play()
		{
	...
			c.Height = background.GetHeight();
			c.Width = background.GetWidth();    

			flappy = new Bird(c, @"C:\images\Flappy.png");
		}
	...

	}
Flappy should now appear in the center of the canvas. The other methods are used to set and get the private variables speed, x and y.

	class Bird
	{
		double speed = 5;
	...
		public void SetSpeed(double newSpeed)
		{
			speed = newSpeed;
		}
		public double GetSpeed()
		{
			return speed;
		}
		public double GetPosX()
		{
			return x;
		}
		public double GetPosY()
		{
			return y;
		}
		public double GetHeight()
		{
			return image.Height;
		}
		public double GetWidth()
		{
			return image.Width;
		}
	}
You get the width and height of Flappy again via the instance variables Height and Width of the CanvasElement class. The Bird class's Move method is even simpler. It simply calls the Move method of the CanvasElement class. pre> ... public void Move() { image.Move(x, y); } ... To move Flappy up and down, you have to program two methods, Up and Down. These simply increase or decrease the y-coordinate by the speed value and then call the Move method.

	...
		public void Up()
		{
			y = y - GetSpeed();
			Move();
		}
		public void Down()
		{
			y = y + GetSpeed();
			Move();
		}
	}
This gives you all the methods you need to move Flappy. You "build" the keyboard control into the Draw method, using the Up and Down methods.

	class Game
	{
		Canvas c;
		Background background;
		Bird flappy;

		public void Play()
		{
	...        
			flappy = new Bird(c, @"C:\images\Flappy.png");
		}

		void Draw()
		{
			background.Move();

			if (c.KeyIsPressed == true)
			{
				if (c.KeyCode == KeyCodes.Up)
					flappy.Up();
				else if (c.KeyCode == KeyCodes.Down)
					flappy.Down();
			}
		}
	}
Now you might be wondering, why isn't the query for the keys done in the Move method?
You could have done that and it would work the same way. However, the class would then not be usable as flexibly as it would only support the keyboard. For example, to move Flappy when you click the mouse button, you can now alternatively use the following programming code.

    void Draw()
    {
...       
        if (c.MouseIsPressed == true)
        {
            var y = flappy.GetPosY();
            if (c.MouseY < y)
                flappy.Up();
            else if (c.MouseY > y)
                flappy.Down();
        }
    }

Finally the pillars

A separate class Pillar should also be created for the pillars. In addition to the already known methods for the speed and the x-coordinate, it should contain a method IsInGap, which is used to check whether an object such as Flappy has flown through the gap.

The Canvas instance is passed in the constructor method in order to create the two rectangles for the pillars on the canvas. You store both rectangles in two instance variables named above and below. Next to that you create two variables for the speed with the name speed and for the x-coordinate.

	class Pillar
	{
		CanvasElement above;
		CanvasElement below;
		double speed = 5;
		double x = -1;
		Canvas c;

		public Pillar(Canvas canvas)
		{
			c = canvas;

			c.RectMode(RectModes.Corner);
			above = c.Rect();
			below = c.Rect();
		}
		public void SetSpeed(double newSpeed)
		{
			speed = newSpeed;
		}
		public double GetSpeed()
		{
			return speed;
		}
		public double GetPosX()
		{
			return x;
		}
	}
The GetSpeed, SetSpeed and GetPosX methods return the corresponding values or set the value of the variable speed or x. The Move method is intended to fulfill two tasks: First, it determines the y-position of the lower rectangle when a new column is to appear in the canvas from the right. On the other hand, it moves the rectangles to the left with the corresponding height.

	class Pillar
	{
	...
		Canvas c;
		double yBelow;
	...
		public void Move()
		{
			if (x < 0)
			{
				x = c.Width;
				yBelow = Number.Random(100, c.Height - 100);
				above.Resize(20, yBelow - 100);
				below.Resize(20, 600 - yBelow);
			}
			above.Move(x, 0);
			below.Move(x, yBelow);

			x = x - 5;
		}
	}
You store the y-coordinate of the lower rectangle in the instance variable yBelow. This is always recalculated when x becomes negative, i.e. the column on the left disappears from the canvas. The variable x is then set to the width of the canvas again, so that the new column can "wander into" the painting area from the right. Because the heights of the two rectangles depend on the yBelow coordinate, the height is set using the CanvasElement class's Resize method.
Otherwise, both pillars are moved 5 pixels to the left with the Move method of the CanvasElement class, i.e. exactly as with the background image, and x is decreased by 5 again.
Finally, you need the IsInGap method, which returns true if the coordinates of a given object fit through the gap.

	class Pillar
	{
	...
		public bool IsInGap(double yBird, double heightBird)
		{
			bool isInGap = yBird >= yBelow - 100 && 
						   yBird <= yBelow - heightBird;

			return isInGap;
		}
	}
To do this, the method needs the y-coordinate of Flappy and its height. If Flappy is then in the gap of 100 pixels, true is stored in the isInGap variable, otherwise false is returned.
Now the class needs to be used in the Game class. To do this, a variable of the Pillar class is created as an instance variable and the object is created in the Play method. In the Draw method you now call the Move method of the Pillar class and the pillar with the two rectangles should appear and move.

	class Game
	{
		Background background;
		Bird flappy;
		Pillar pillar;
	...
		public void Play()
		{
	...
			flappy = new Bird(c, @"C:\images\Flappy.png");
			pillar = new Pillar(c);
		}

		void Draw()
		{
			background.Move();
			pillar.Move();        
	...
		}
	}
The only thing missing now is checking whether Flappy flew through the gap. To do this, first check whether Flappy has approached the pillar horizontally. If this is the case, the IsInGap method can be used to check whether it is within the gap.

		void Draw()
		{
	...
			double flappyX = flappy.GetPosX();
			double flappyY = flappy.GetPosY();
			double pillarX = pillar.GetPosX();

			if (flappyX >= pillarX && 
				flappyX <= pillarX + pillar.GetSpeed())
			{
				bool isInGap = pillar.IsInGap(
										flappyY, flappy.GetHeight());
				if (isInGap)
					correct = correct + 1;
				else
					wrong = wrong + 1;

				text.Text($"good {correct} / bad {wrong}");
			}
		}
	}
The counting of whether Flappy was successful or not is already built into the Draw method. In order for the programming code to work, you still have to create the two instance variables correct and wrong of the data type int and another instance variable text of the data type CanvasElement to display the text. In the Play method, you then create the object using the Text method of the Canvas class.

	class Game
	{
	...
		Pillar pillar;
		int correct, wrong;
		CanvasElement text;

		public void Play()
		{
	...
			pillar = new Pillar(c);
			text = c.Text("");
		}
	...
That was it! Everything should be working again. However, your programming code has roughly tripled compared to the last Flappy version. When you program object-oriented, your programming code usually gets bigger, but it should also be more understandable, flexible, and maintainable. Of course, whether your programming code really fulfills this depends on how well you have planned the classes. However, this also takes time, a lot of practice and looking at programming code from advanced programmers.
For the actual game that you programmed in the Game class, however, there are fewer lines of code. In addition, the programming code should also be more understandable due to the specific classes Background, Pillar and Bird. The programming code is more flexible because the individual classes only contain the data and methods that are important to them and these cannot easily be changed from the outside and are also not used by several classes at the same time. I want to show you that again by animating Flappy and making it "flap".

...now Flappy is flapping

So far, Flappy's wings aren't moving. Now you should change that by using 4 pictures of Flappy, in which the position of the wings changes slightly.

Each time the Draw method is called again, it should switch to the next frame, giving the impression that the wings are moving. In contrast to the non-object-oriented solution, you now only have to make changes in the programming code of the Bird class, apart from instantiating the Bird class.
First you need an additional variable currentBirdFrame of the data type int in the Bird class. In currentBirdFrame you store the currently displayed image. In the constructor method you check whether the file path contains an "*". I chose the asterisk as a placeholder for the number in the file name. You replace this asterisk with the currentBirdFrame variable after you have increased it. You also store the file path with the asterisk in the instance variable pathBird. By the way, a character string is also a class and contains its own methods, such as Replace. Even an integer has its own methods like ToString to convert the number to a string. These two methods are used to replace the asterisk with the currentBirdFrame variable.

	class Bird
	{
	...
		double speed = 5;
		int currentBirdFrame = -1;
		string pathBird;

		public Bird(Canvas c, string path)
		{
			if (path.Contains('*') == true)
			{
				pathBird = path;
				currentBirdFrame++;
				path = pathBird.Replace("*", currentBirdFrame.ToString());
			}

			x = c.Width / 2;
			y = c.Height / 2;

			image = c.Image(path, x, y);
		} 
	...
All further changes happen in the Move method of the class. There it is first checked whether currentBirdFrame is set to -1. Then the file path did not contain an asterisk and the program works with only one image as before. Alternatively, you could also explicitly test for an asterisk using the Contains method of the string class. Otherwise currentBirdFrame is increased by 1 and the file path is generated again using the Replace method. In order not to be limited to 4 animation images, use the System.IO.File.Exists method to check whether the image file even exists. If this is not the case, you simply set the currentBirdFrame variable back to 0 to start the animation from the beginning.

	...
		public void Move()
		{
			image.Move(x, y);

			if (currentBirdFrame != -1)
			{
				currentBirdFrame++;
				string path = pathBird.Replace("*", currentBirdFrame.ToString());
				if (System.IO.File.Exists(path) == false)
				{
					currentBirdFrame = 0;
					path = pathBird.Replace("*", currentBirdFrame.ToString());
				}
				image.Image(path);
			}
		} 
	...
All that's missing now is a small change in creating the Bird class in the Play method. There you must now change the file path so that it contains the star as a placeholder symbol and the corresponding image files are located in this folder.

	class Game
	{
	...
		public void Play()
		{
	...
			c.Height = background.GetHeight();
			c.Width = background.GetWidth();    

			flappy = new Bird(c, @"C:\images\Flappy\Flappy*.png");
			pillar = new Pillar(c);
			text = c.Text("");
		}
	...
When you start the Flappy game, Flappy will flap whenever you move it up or down. This is because the Move method is only called when either button is pressed. If you always want Flappy to flap, add the Move method to the start of the Draw method. Then the Move method is called every time the Draw method is called.

Download source code

 This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License