📜 ⬆️ ⬇️

Impressive Solids: making a game in C # under OpenGL, part I

Once upon a time in america


Once, in 2002, an interesting toy called Amazing Blocks came to my computer. The game, so to speak, Tetris class (a detailed description of the gameplay below); She was very fond of my mother, who played the game for hours. However, there was an annoying drawback: after what seems to be 10 launches, the game began to require registration, which, surprisingly, was free, but through the Internet, which, of course, was an insurmountable obstacle, since there was no Internet then. , although they heard that there is such a thing. I had to constantly reinstall.

Three years later, when the Internet was already held, and the game managed to become shareware and start asking for registration for some money, I tried to register it, however, the manufacturer’s website was dead rather than alive by that time, and apparently remains so and to this day. The shareware-version of the game is easily found on the Internet, a lot of, I’m not afraid of this word, keygens, which are actually trojans, and not a single opportunity to register the game so that mom can play it on a completely different computer. At some point I thought: why not just make the same game myself and solve the problem at the root? At the same time, this can result in some kind of hello-world in developing a simple PC game in modern conditions - which I offer to the attention of readers.

image So, what kind of game are we going to do? The bottom line is this. In a rectangular glass 7 × 13, a horizontal stick consisting of 3 color blocks falls (there are 5 colors in total). While moving, it can be moved left and right, as well as interchanged blocks in rotation from right to left (red, green, blue → green, blue, red). As soon as the stick touches the floor of the glass or any of the fixed blocks in the glass, it can no longer be controlled. The blocks that make up the stick continue to fall apart until they are placed on a fixed block or half a glass. After that, it is checked whether a horizontal, vertical or diagonal line of three or more blocks of the same color has turned out in the glass; such lines are destroyed. If there were blocks on top of the destroyed line, they slide down to the empty space formed, then the formed lines are destroyed again. When everything settles down, a new stick begins to fall from above. For building lines destroyed player gets points. The game ends when the glass is filled to the top.
')
Technology. The game will be done in C # (I have long wanted to see what it is), OpenGL (DirectX only works under Windows, but I prefer Linux more), Mercurial for version control (writing code without VCS is disrespect for myself).

The game will be called Impressive Solids.


Inception


Development under Windows will be conducted in Microsoft Visual C # 2010 Express (distributed for free). We also need TortoiseHg , the Windows client of the Mercurial version control system. Under Linux-based systems, we will use MonoDevelop and console hg.

To connect OpenGL, we use binding OpenTK . You need to download a fresh nightly build (at the time of writing: 2011-12-03).

Create a new empty project in Visual C # Express called ImpressiveSolids. We save. Then open the project directory, call the context menu for it and select TortoiseHg → Create Repository Here. We mark the points of creating the .hgignore file and opening the workbench after initialization.

Open the .hgignore file in Visual C # Express and write the following lines to it. This is necessary so that the version control system does not take into account unnecessary binary files.

syntax: glob *.suo *.pidb ImpressiveSolids/bin/* ImpressiveSolids/obj/* 


Inside the solution directory (not the project; where the .hgignore is located), create an OpenTK subdirectory and copy the OpenTK * .dll and OpenTK * .dll.config files from the opentk \ Binaries \ OpenTK \ Release \ directory in the OpenTK archive into it.

In Visual C # Express, the context menu is References → Add Reference → Browse. Choose ../OpenTK/OpenTK.dll. In addition, you need to add reference to System.Drawing from the .NET tab.

Create a new class Game . This is the main class of the program, it contains the entry point, and it is the heir to OpenTK.GameWindow and is responsible for updating the game state ( OnUpdateFrame ) and redrawing ( OnRenderFrame ). Now it will be just a black window.

  using System; using OpenTK; using OpenTK.Graphics; using OpenTK.Graphics.OpenGL; namespace ImpressiveSolids { class Game : GameWindow { [STAThread] static void Main() { using (var Game = new Game()) { Game.Run(30); } } public Game() : base(700, 500, GraphicsMode.Default, "Impressive Solids") { VSync = VSyncMode.On; } protected override void OnLoad(EventArgs E) { base.OnLoad(E); } protected override void OnResize(EventArgs E) { base.OnResize(E); GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height); } protected override void OnUpdateFrame(FrameEventArgs E) { base.OnUpdateFrame(E); } protected override void OnRenderFrame(FrameEventArgs E) { base.OnRenderFrame(E); GL.ClearColor(Color4.Black); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); SwapBuffers(); } } } 


Go to the project properties (Project → ImpressiveSolids Properties) and specify the Target framework: .NET Framework 2.0; Output type: Windows Application; Startup object: ImpressiveSolids.Game.

You can save and run, a black window with a size of 700 × 500 should appear with the title “Impressive Solids”.

If everything went smoothly, go to TortoiseHg Workbench and commit everything with the mark “Initial game window”.

The fall


We realize the managed falling of a stick. To do this, you first need to set the model of the current state of the stick. First, the position. By default - the top center of the glass. We assume that (0; 0) corresponds to the upper left corner of the glass. It is necessary, by the way, to set its dimensions MapWidth , MapHeight . The colors of the blocks that make up the stick will be stored as an array of integers; set the number of possible colors ColorsCount and agree that the color is indicated by an integer value from 0 to ColorsCount − 1 .

Add a New method to the Game class and call it from OnLoad. In this method we implement the construction of a stick from blocks of random colors.

  private Random Rand; private const int MapWidth = 7; private const int MapHeight = 13; private const int StickLength = 3; private int[] StickColors; private Vector2 StickPosition; private const int ColorsCount = 5; protected override void OnLoad(EventArgs E) { base.OnLoad(E); New(); } private void New() { Rand = new Random(); StickColors = new int[StickLength]; for (var i = 0; i < StickLength; i++) { StickColors[i] = Rand.Next(ColorsCount); } StickPosition.X = (float)Math.Floor((MapWidth - StickLength) / 2d); StickPosition.Y = 0; } 


Let's try to display our stick on the screen, while in the most primitive version (the block is represented by a colored rectangle, the glass is started right in the upper left corner of the window). Let's make some changes to the code.

  private const int NominalWidth = 700; private const int NominalHeight = 500; private float ProjectionWidth; private float ProjectionHeight; private const int SolidSize = 35; private Color4[] Colors = {Color4.PaleVioletRed, Color4.LightSeaGreen, Color4.CornflowerBlue, Color4.RosyBrown, Color4.LightGoldenrodYellow}; public Game() : base(NominalWidth, NominalHeight, GraphicsMode.Default, "Impressive Solids") { VSync = VSyncMode.On; } protected override void OnResize(EventArgs E) { base.OnResize(E); GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height); ProjectionWidth = NominalWidth; ProjectionHeight = (float)ClientRectangle.Height / (float)ClientRectangle.Width * ProjectionWidth; if (ProjectionHeight < NominalHeight) { ProjectionHeight = NominalHeight; ProjectionWidth = (float)ClientRectangle.Width / (float)ClientRectangle.Height * ProjectionHeight; } } protected override void OnRenderFrame(FrameEventArgs E) { base.OnRenderFrame(E); GL.ClearColor(Color4.Black); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); var Projection = Matrix4.CreateOrthographic(-ProjectionWidth, -ProjectionHeight, -1, 1); GL.MatrixMode(MatrixMode.Projection); GL.LoadMatrix(ref Projection); GL.Translate(ProjectionWidth / 2, -ProjectionHeight / 2, 0); var Modelview = Matrix4.LookAt(Vector3.Zero, Vector3.UnitZ, Vector3.UnitY); GL.MatrixMode(MatrixMode.Modelview); GL.LoadMatrix(ref Modelview); GL.Begin(BeginMode.Quads); for (var i = 0; i < StickLength; i++) { RenderSolid(StickPosition.X + i, StickPosition.Y, StickColors[i]); } GL.End(); SwapBuffers(); } private void RenderSolid(float X, float Y, int Color) { GL.Color4(Colors[Color]); GL.Vertex2(X * SolidSize, Y * SolidSize); GL.Vertex2((X + 1) * SolidSize, Y * SolidSize); GL.Vertex2((X + 1) * SolidSize, (Y + 1) * SolidSize); GL.Vertex2(X * SolidSize, (Y + 1) * SolidSize); } 


Tricks with Nominal/Projection Width/Height needed to image scaled when resizing the window, but at the same time, the proportions are not distorted.

Now let's finally make the stick fall and so that the ←, →, ↑ keys (color rotation) work.

  public Game() : base(NominalWidth, NominalHeight, GraphicsMode.Default, "Impressive Solids") { VSync = VSyncMode.On; Keyboard.KeyDown += new EventHandler<KeyboardKeyEventArgs>(OnKeyDown); } protected override void OnUpdateFrame(FrameEventArgs E) { base.OnUpdateFrame(E); StickPosition.Y += 0.02f; } protected void OnKeyDown(object Sender, KeyboardKeyEventArgs E) { if (Key.Left == E.Key) { --StickPosition.X; } else if (Key.Right == E.Key) { ++StickPosition.X; } else if (Key.Up == E.Key) { var T = StickColors[0]; for (var i = 0; i < StickLength - 1; i++) { StickColors[i] = StickColors[i + 1]; } StickColors[StickLength - 1] = T; } } 


Let's commit all changes: “The stick, falling and controllable”.

As you can see, there is no check to go beyond the boundaries of the glass. We will correct this omission in the future.

A Map of the World


Let us deal with the situation when the stick fell on the floor or on the blocks already present in the glass. For now, we will not deal with the subsequent uncontrolled fall of the blocks and the destruction of the lines, but we will simply make the components of the stick freeze in place (even hanging in the air) and the next stick will begin to fall.

We simulate the state of the glass as a two-dimensional array of integers. The coordinates will correspond to the checkered grid of the glass, the values ​​will be the color of the block in this cell - or a negative number if the cell is empty.

Here it will be necessary to enter a check for the stick out of the glass, otherwise there will be calls to the array on non-existent indices.

  private int[,] Map; private void New() { Rand = new Random(); Map = new int[MapWidth, MapHeight]; for (var X = 0; X < MapWidth; X++) { for (var Y = 0; Y < MapHeight; Y++) { Map[X, Y] = -1; } } StickColors = new int[StickLength]; GenerateNextStick(); } private void GenerateNextStick() { for (var i = 0; i < StickLength; i++) { StickColors[i] = Rand.Next(ColorsCount); } StickPosition.X = (float)Math.Floor((MapWidth - StickLength) / 2d); StickPosition.Y = 0; } protected override void OnUpdateFrame(FrameEventArgs E) { base.OnUpdateFrame(E); StickPosition.Y += 0.02f; var FellOnFloor = (StickPosition.Y >= MapHeight - 1); var FellOnBlock = false; if (!FellOnFloor) { var Y = (int)Math.Floor(StickPosition.Y + 1); for (var i = 0; i < StickLength; i++) { var X = (int)StickPosition.X + i; if (Map[X, Y] >= 0) { FellOnBlock = true; break; } } } if (FellOnFloor || FellOnBlock) { var Y = (int)Math.Floor(StickPosition.Y); for (var i = 0; i < StickLength; i++) { var X = (int)StickPosition.X + i; Map[X, Y] = StickColors[i]; } GenerateNextStick(); } } protected void OnKeyDown(object Sender, KeyboardKeyEventArgs E) { if ((Key.Left == E.Key) && (StickPosition.X > 0)) { --StickPosition.X; } else if ((Key.Right == E.Key) && (StickPosition.X + StickLength < MapWidth)) { ++StickPosition.X; } else if (Key.Up == E.Key) { // . . . } } protected override void OnRenderFrame(FrameEventArgs E) { // . . . GL.Begin(BeginMode.Quads); for (var X = 0; X < MapWidth; X++) { for (var Y = 0; Y < MapHeight; Y++) { if (Map[X, Y] >= 0) { RenderSolid(X, Y, Map[X, Y]); } } } for (var i = 0; i < StickLength; i++) { RenderSolid(StickPosition.X + i, StickPosition.Y, StickColors[i]); } GL.End(); SwapBuffers(); } 


Now you can quickly sketch blocks to the very top, as in the good old Tetris. For testing, you can increase the fall rate by replacing 0.02f with 0.2f , but in general it will be necessary to make it possible to accelerate by pressing the ↓ key.

Do not forget to commit changes to the Mercurial repository: "Fixing blocks after the stick fell".

Double impact


The next thing we need to do is to ensure that the blocks do not hang in the air, but continue to fall down until they abut. During this period of time there is no stick on the screen, you cannot control anything. In this regard, we introduce into the game the concept of state.

The game at any time is in one of the following states:
  1. The next stick falls, it can be managed. When a new game begins, this state is activated.
  2. Uncontrollable falling of blocks, destruction of lined lines. This state turns on after the stick has touched a block. It ends when all blocks are stationary and there are no lines to be destroyed. If the entire top row of the glass is free, then the game continues in state No. 1; otherwise, the game ends (state number 3).
  3. The game is over, nothing happens. A player can start a new game (for example, by pressing a certain button).

We make the appropriate ads in the code.

  private enum GameStateEnum { Fall, Impact, GameOver } private GameStateEnum GameState; private void New() { // . . . GenerateNextStick(); GameState = GameStateEnum.Fall; } protected override void OnUpdateFrame(FrameEventArgs E) { base.OnUpdateFrame(E); if (GameStateEnum.Fall == GameState) { StickPosition.Y += 0.2f; // . . . if (FellOnFloor || FellOnBlock) { var Y = (int)Math.Floor(StickPosition.Y); for (var i = 0; i < StickLength; i++) { var X = (int)StickPosition.X + i; Map[X, Y] = StickColors[i]; } GameState = GameStateEnum.Impact; } } else if (GameStateEnum.Impact == GameState) { var Stabilized = true; // TODO   if (Stabilized) { GenerateNextStick(); GameState = GameStateEnum.Fall; } } } 


In order to depict the smooth fall of the blocks and not complicate the model of the glass ( Map ), we will resort to tricks. Let the block that falls from the cell (X; Y) into the cell (X; Y + 1) - but where else should it fall? - is listed in the cage (X; Y) until the moment of the final entry into the lower cage; and we will additionally store a fractional offset of the block vertically, which will gradually increase until it exceeds one. That is, the real coordinates of the block will not be (X; Y), but (X; Y + Δ), this will need to be OnRenderFrame into account in OnRenderFrame .

  private const float FallSpeed = 0.2f; private float[,] ImpactFallOffset; private void New() { // . . . ImpactFallOffset = new float[MapWidth, MapHeight]; } protected override void OnUpdateFrame(FrameEventArgs E) { base.OnUpdateFrame(E); if (GameStateEnum.Fall == GameState) { StickPosition.Y += FallSpeed; // . . . } else if (GameStateEnum.Impact == GameState) { var Stabilized = true; for (var X = 0; X < MapWidth; X++) { for (var Y = MapHeight - 2; Y >= 0; Y--) { if ((Map[X, Y] >= 0) && ((Map[X, Y + 1] < 0) || (ImpactFallOffset[X, Y + 1] > 0))) { Stabilized = false; ImpactFallOffset[X, Y] += FallSpeed; if (ImpactFallOffset[X, Y] >= 1) { Map[X, Y + 1] = Map[X, Y]; Map[X, Y] = -1; ImpactFallOffset[X, Y] = 0; } } } } if (Stabilized) { GenerateNextStick(); GameState = GameStateEnum.Fall; } } } protected override void OnRenderFrame(FrameEventArgs E) { // . . . GL.Begin(BeginMode.Quads); for (var X = 0; X < MapWidth; X++) { for (var Y = 0; Y < MapHeight; Y++) { if (Map[X, Y] >= 0) { RenderSolid(X, Y + ImpactFallOffset[X, Y], Map[X, Y]); } } } if (GameStateEnum.Fall == GameState) { for (var i = 0; i < StickLength; i++) { RenderSolid(StickPosition.X + i, StickPosition.Y, StickColors[i]); } } GL.End(); SwapBuffers(); } 


In order to fall at once, a whole column of blocks under which a hole was formed, we look up the card cells from bottom to top (the lower block carries the upper one) and take into account not only the void of the cell, but also the status ImpactFallOffset (because a positive number means that this block falls down ).

Make a note with the note: "Blocks fall from impact until stabilized."

Weapon of Mass Destruction


It is time to finally deal with the main element of the gameplay: the destruction of lined lines of the same color. Set the minimum length of the line. It is enough to go through all the cells in which there are blocks, and from each try to build a line in one of four possible directions (horizontal, vertical and two diagonals). If a line of the same color of sufficient length is found, each component of its block is put on the stack. After the entire map has been verified, we destroy all blocks placed on the stack and mark the position as unstable.

  private const int DestroyableLength = 3; private Stack<Vector2> Destroyables = new Stack<Vector2>(); protected override void OnUpdateFrame(FrameEventArgs E) { // . . . } else if (GameStateEnum.Impact == GameState) { // . . . if (Stabilized) { Destroyables.Clear(); for (var X = 0; X < MapWidth; X++) { for (var Y = 0; Y < MapHeight; Y++) { CheckDestroyableLine(X, Y, 1, 0); CheckDestroyableLine(X, Y, 0, 1); CheckDestroyableLine(X, Y, 1, 1); CheckDestroyableLine(X, Y, 1, -1); } } if (Destroyables.Count > 0) { foreach (var Coords in Destroyables) { Map[(int)Coords.X, (int)Coords.Y] = -1; } Stabilized = false; } } if (Stabilized) { GenerateNextStick(); GameState = GameStateEnum.Fall; } } } private void CheckDestroyableLine(int X1, int Y1, int DeltaX, int DeltaY) { if (Map[X1, Y1] < 0) { return; } int X2 = X1, Y2 = Y1; var LineLength = 0; while ((X2 >= 0) && (Y2 >= 0) && (X2 < MapWidth) && (Y2 < MapHeight) && (Map[X2, Y2] == Map[X1, Y1])) { ++LineLength; X2 += DeltaX; Y2 += DeltaY; } if (LineLength >= DestroyableLength) { for (var i = 0; i < LineLength; i++) { Destroyables.Push(new Vector2(X1 + i * DeltaX, Y1 + i * DeltaY)); } } } 


In the repository we mark: “Destroying lines of the same color”.

Game over


You probably noticed the funny behavior of the game when you reach the top of the glass: at the top they start to blink in different colors, replacing each other, endlessly appearing and immediately sticking sticks. Let's handle the loss situation: nothing will happen until the user presses the Enter key.

Everything is simple here.

  protected override void OnUpdateFrame(FrameEventArgs E) { // . . . } else if (GameStateEnum.Impact == GameState) { // . . . if (Stabilized) { var GameOver = false; for (var X = 0; X < MapWidth; X++) { if (Map[X, 0] >= 0) { GameOver = true; break; } } if (GameOver) { GameState = GameStateEnum.GameOver; } else { GenerateNextStick(); GameState = GameStateEnum.Fall; } } } } protected void OnKeyDown(object Sender, KeyboardKeyEventArgs E) { if (GameStateEnum.Fall == GameState) { if ((Key.Left == E.Key) && (StickPosition.X > 0)) { --StickPosition.X; } else if ((Key.Right == E.Key) && (StickPosition.X + StickLength < MapWidth)) { ++StickPosition.X; } else if (Key.Up == E.Key) { var T = StickColors[0]; for (var i = 0; i < StickLength - 1; i++) { StickColors[i] = StickColors[i + 1]; } StickColors[StickLength - 1] = T; } } else if (GameStateEnum.GameOver == GameState) { if ((Key.Enter == E.Key) || (Key.KeypadEnter == E.Key)) { New(); } } } 


Let's commit, without superfluous thinking, by signing: “Game over”.

On it the first part of development is finished. We have on hand - a fully-featured game that you can already play. In the second part, we will deal with the design. We will overlay the textures, we will show the current score, a record score; the stick that comes up next. Finally, we will center the image of the glass in the game window, draw its borders. In general, we will bring everything to mind.

The project is available on Google Project Hosting Bitbucket , there you can see the final source code, download the archive with an executable file ready for launch.

Source: https://habr.com/ru/post/133983/


All Articles