📜 ⬆️ ⬇️

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

Screenshot decorated game In the first part of the development of the tetris-like game Impressive Solids, we implemented the main part of the gameplay, paying minimal attention to the appearance of the application. We almost did not use OpenGL, we just made it up that we painted a colored rectangle. It is time to do the design, as well as to realize the scoring and storage of a record (high score). Well, let's go further.

Picture this


Let's do the textures. We need, first, to pull something on the background of the window, and second, to make nice-looking blocks (now these are just colored rectangles). Understandably, you first need to make textures. GIMP will help us in this. If you do not want to engage in graphics, you can simply download the archive with ready-made textures and proceed to the next step.

But first I will note one very important nuance. Prior to OpenGL 2.0, each of the texture sizes had to be equal to a power of two (i.e. 64 Ă— 64, 512 Ă— 256; these are POT textures, from the English of power of two). If textures of arbitrary size (NPOT textures) are not supported by a video card or a video card driver, such a texture will not work. This is the case, for example, for Intel integrated graphics cards under Windows XP.
')
To ensure that you are safe from this problem, the simplest and most convenient solution is to always use POT textures. However, it is not always possible, and further, when we get to the output of the text, we will have to deal with this moment.

So, create in GIMP an empty (white) image of 512 × 512, then: Filters → Artistic → Apply Canvas, then: Filters → Map → Make Seamless. Everything background.png is ready.

We will try to depict the blocks as marble balls, Daniel Ketchum will help us in this. Create a transparent image somewhere 300 × 300, make a round selection for the entire diameter of the canvas. Tool Bucket Fill → Pattern fill → select the texture Marble # 1, fill the circle. Next: Filters → Distort → Lens Distortion, turn Main to maximum, and Edge to minimum, OK. Then: Filters → Light and Shadow → Lighting Effects, set the light so as to create the effect of a three-dimensional ball. We crop so that there are no empty fields. Scale to size 256 × 256. Then with the help of Colors → Colorize we make five different colors (we twist Hue) and save as 0.png, 1.png ... 4.png (remember that in the game model we decided to designate different colors of the blocks as an integer starting from zero).

Now you need to add these files to the Visual C # Express / MonoDevelop project. First, through the context menu, create a New Folder called textures, and in it - solids. Through the file manager, put the background.png file in textures, and the 0.png files in textures / solids ... 4.png. Through the context menu in the development environment, we add these files to the project in the appropriate folders.

After that, it is necessary to open Properties for all * .png files in the project and set Build Action: Content; Copy to Output Directory: Copy if newer.

Enable textures in OpenGL. OpenGL itself does not operate graphic files, you need your own means (this will help System.Drawing.Bitmap ) load the texture into memory, get a binary bitmap from it and transfer it to OpenGL, which will save the texture already in its memory. In the future, the texture can be accessed through an integer handle (which must first be reserved).

We encapsulate this mechanism in the form of a new class Texture .

 using System; using System.Drawing; using System.Drawing.Imaging; using OpenTK.Graphics.OpenGL; namespace ImpressiveSolids { public class Texture : IDisposable { public int GlHandle { get; protected set; } public int Width { get; protected set; } public int Height { get; protected set; } public Texture(Bitmap Bitmap) { GlHandle = GL.GenTexture(); Bind(); Width = Bitmap.Width; Height = Bitmap.Height; var BitmapData = Bitmap.LockBits(new Rectangle(0, 0, Bitmap.Width, Bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, BitmapData.Width, BitmapData.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, BitmapData.Scan0); Bitmap.UnlockBits(BitmapData); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); } public void Bind() { GL.BindTexture(TextureTarget.Texture2D, GlHandle); } #region Disposable private bool Disposed = false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool Disposing) { if (!Disposed) { if (Disposing) { GL.DeleteTexture(GlHandle); } Disposed = true; } } ~Texture() { Dispose(false); } #endregion } } 


In the Game will load the textures.

 using System.Drawing; // . . . private Texture TextureBackground; private Texture[] ColorTextures = new Texture[ColorsCount]; public Game() : base(NominalWidth, NominalHeight, GraphicsMode.Default, "Impressive Solids") { VSync = VSyncMode.On; Keyboard.KeyDown += new EventHandler<KeyboardKeyEventArgs>(OnKeyDown); TextureBackground = new Texture(new Bitmap("textures/background.png")); for (var i = 0; i < ColorsCount; i++) { ColorTextures[i] = new Texture(new Bitmap("textures/solids/" + i + ".png")); } } 


Change the rendering. It is necessary to include textures in general, as well as transparency mode. Then, before you specify the coordinates of the rectangle, you must select (bind) the corresponding texture. Before each point (vertex) it is necessary to specify the corresponding coordinates of the texture (it is assumed that (1; 1) is the lower right corner of the texture).

 protected override void OnLoad(EventArgs E) { base.OnLoad(E); GL.Enable(EnableCap.Texture2D); GL.Enable(EnableCap.Blend); GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha); New(); } protected override void OnRenderFrame(FrameEventArgs E) { // . . . GL.LoadMatrix(ref Modelview); RenderBackground(); 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]); } } SwapBuffers(); } private void RenderBackground() { TextureBackground.Bind(); GL.Color4(Color4.White); GL.Begin(BeginMode.Quads); GL.TexCoord2(0, 0); GL.Vertex2(0, 0); GL.TexCoord2((float)ClientRectangle.Width / TextureBackground.Width, 0); GL.Vertex2(ProjectionWidth, 0); GL.TexCoord2((float)ClientRectangle.Width / TextureBackground.Width, (float)ClientRectangle.Height / TextureBackground.Height); GL.Vertex2(ProjectionWidth, ProjectionHeight); GL.TexCoord2(0, (float)ClientRectangle.Height / TextureBackground.Height); GL.Vertex2(0, ProjectionHeight); GL.End(); } private void RenderSolid(float X, float Y, int Color) { ColorTextures[Color].Bind(); GL.Color4(Color4.White); GL.Begin(BeginMode.Quads); GL.TexCoord2(0, 0); GL.Vertex2(X * SolidSize, Y * SolidSize); GL.TexCoord2(1, 0); GL.Vertex2((X + 1) * SolidSize, Y * SolidSize); GL.TexCoord2(1, 1); GL.Vertex2((X + 1) * SolidSize, (Y + 1) * SolidSize); GL.TexCoord2(0, 1); GL.Vertex2(X * SolidSize, (Y + 1) * SolidSize); GL.End(); } 


The specified color colors, tints the texture, so we indicate white.

Fine, textures work. Commit: "Textured background and solids".

Main street


Now it is necessary to designate a glass. Let it be just a black rectangle, over the background of the window, but behind the blocks.

 private void RenderPipe() { GL.Disable(EnableCap.Texture2D); GL.Color4(Color4.Black); GL.Begin(BeginMode.Quads); GL.Vertex2(0, 0); GL.Vertex2(MapWidth * SolidSize, 0); GL.Vertex2(MapWidth * SolidSize, MapHeight * SolidSize); GL.Vertex2(0, MapHeight * SolidSize); GL.End(); GL.Enable(EnableCap.Texture2D); } protected override void OnRenderFrame(FrameEventArgs E) { // . . . RenderBackground(); RenderPipe(); // . . . } 


Let's position the glass. Let it be on the left, with a small indent from the edges of the window. To the right of the glass later we will place additional interface elements (score, etc.). However, if the window is stretched in width, then let the glass slide to the right towards the center of the window, otherwise there will be too much empty space to the right. Finally, we NominalWidth making the window smaller than NominalWidth Ă— NominalHeight (this, however, will not work under the X window system).

 private const int NominalWidth = 500; private const int NominalHeight = 500; protected override void OnResize(EventArgs E) { // . . . if (ClientSize.Width < NominalWidth) { ClientSize = new Size(NominalWidth, ClientSize.Height); } if (ClientSize.Height < NominalHeight) { ClientSize = new Size(ClientSize.Width, NominalHeight); } } protected override void OnRenderFrame(FrameEventArgs E) { // . . . RenderBackground(); var PipeMarginY = (ProjectionHeight - MapHeight * SolidSize) / 2f; var PipeMarginX = (NominalHeight - MapHeight * SolidSize) / 2f; var Overwidth = ProjectionWidth - ProjectionHeight * (float)NominalWidth / NominalHeight; if (Overwidth > 0) { GL.Translate(Math.Min(Overwidth, (ProjectionWidth - MapWidth * SolidSize) / 2f), PipeMarginY, 0); } else { GL.Translate(PipeMarginX, PipeMarginY, 0); } RenderPipe(); // . . . } 


Commit: "Position and render pipe".

Writing on the Wall


What will be to the right of the glass? Four elements: the next stick; game status (there will be a message "Playing", "Paused" or "Game Over"); current account; record score

Most of this is text, so we need to learn how to draw text using OpenGL. OpenGL itself is not able to do anything like that. Often there are mentions that OpenTK has a convenient class TextPrinter for this purpose. It was a long time and not true. Now the recommended method for displaying text is the following: make a bitmap with text (using System.Drawing.Graphics.DrawString or others) and drag it as a texture.

Let's write our TextRenderer class, which will create a Bitmap and then Texture based on it. But first you have to attend to the aforementioned problem of NPOT-dimensional textures, since we do not know in advance what size the dynamically created label will have. The method is quite simple: if NPOT-textures are not supported, then when loading a picture, you need to make a POT-dimensional texture, as if with fields. For example, if we load a 300 Ă— 200 image, then we generate a 512 Ă— 256 texture, on which our image will be in the upper left corner, and the rest of the space will be empty. And when applying a texture, it will be necessary to take into account that the lower left corner of the picture has the coordinates not (1; 1), but (300/512; 200/256).

 public class Texture : IDisposable { public int GlHandle { get; protected set; } public int Width { get; protected set; } public int Height { get; protected set; } #region NPOT private static bool? CalculatedSupportForNpot; public static bool NpotIsSupported { get { if (!CalculatedSupportForNpot.HasValue) { CalculatedSupportForNpot = false; int ExtensionsCount; GL.GetInteger(GetPName.NumExtensions, out ExtensionsCount); for (var i = 0; i < ExtensionsCount; i++) { if ("GL_ARB_texture_non_power_of_two" == GL.GetString(StringName.Extensions, i)) { CalculatedSupportForNpot = true; break; } } } return CalculatedSupportForNpot.Value; } } public int PotWidth { get { return NpotIsSupported ? Width : (int)Math.Pow(2, Math.Ceiling(Math.Log(Width, 2))); } } public int PotHeight { get { return NpotIsSupported ? Height : (int)Math.Pow(2, Math.Ceiling(Math.Log(Height, 2))); } } #endregion public Texture(Bitmap Bitmap) { // . . . var BitmapData = Bitmap.LockBits(new Rectangle(0, 0, Bitmap.Width, Bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, PotWidth, PotHeight, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, IntPtr.Zero); GL.TexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, BitmapData.Width, BitmapData.Height, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, BitmapData.Scan0); Bitmap.UnlockBits(BitmapData); // . . . } // . . . } 


Now - TextRenderer . The code seems long, but actually everything is simple. Here we do four main things: we set the parameters of the text, measure its dimensions, draw the text into the texture and, finally, apply the texture to the transparent rectangle.

 using System; using System.Drawing; using System.Drawing.Text; using OpenTK.Graphics; using OpenTK.Graphics.OpenGL; namespace ImpressiveSolids { class TextRenderer { private Font FontValue; private string LabelValue; private bool NeedToCalculateSize, NeedToRenderTexture; private Texture Texture; private int CalculatedWidth, CalculatedHeight; public Font Font { get { return FontValue; } set { FontValue = value; NeedToCalculateSize = true; NeedToRenderTexture = true; } } public string Label { get { return LabelValue; } set { if (value != LabelValue) { LabelValue = value; NeedToCalculateSize = true; NeedToRenderTexture = true; } } } public int Width { get { if (NeedToCalculateSize) { CalculateSize(); } return CalculatedWidth; } } public int Height { get { if (NeedToCalculateSize) { CalculateSize(); } return CalculatedHeight; } } public Color4 Color = Color4.Black; public TextRenderer(Font Font) { this.Font = Font; } public TextRenderer(Font Font, Color4 Color) { this.Font = Font; this.Color = Color; } public TextRenderer(Font Font, string Label) { this.Font = Font; this.Label = Label; } public TextRenderer(Font Font, Color4 Color, string Label) { this.Font = Font; this.Color = Color; this.Label = Label; } private void CalculateSize() { using (var Bitmap = new Bitmap(1, 1)) { using (Graphics Graphics = Graphics.FromImage(Bitmap)) { var Measures = Graphics.MeasureString(Label, Font); CalculatedWidth = (int)Math.Ceiling(Measures.Width); CalculatedHeight = (int)Math.Ceiling(Measures.Height); } } NeedToCalculateSize = false; } public void Render() { if ((null == Label) || ("" == Label)) { return; } if (NeedToRenderTexture) { using (var Bitmap = new Bitmap(Width, Height)) { var Rectangle = new Rectangle(0, 0, Bitmap.Width, Bitmap.Height); using (Graphics Graphics = Graphics.FromImage(Bitmap)) { Graphics.Clear(System.Drawing.Color.Transparent); Graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; Graphics.DrawString(Label, Font, Brushes.White, Rectangle); if (null != Texture) { Texture.Dispose(); } Texture = new Texture(Bitmap); } } NeedToRenderTexture = false; } Texture.Bind(); GL.Color4(Color); GL.Begin(BeginMode.Quads); GL.TexCoord2(0, 0); GL.Vertex2(0, 0); GL.TexCoord2((float)Texture.Width / Texture.PotWidth, 0); GL.Vertex2(Width, 0); GL.TexCoord2((float)Texture.Width / Texture.PotWidth, (float)Texture.Height / Texture.PotHeight); GL.Vertex2(Width, Height); GL.TexCoord2(0, (float)Texture.Height / Texture.PotHeight); GL.Vertex2(0, Height); GL.End(); } } } 


Let's output something to the right of the glass.

 using System.Drawing.Text; // . . . private int Score; private int HighScore; private TextRenderer NextStickLabel, ScoreLabel, ScoreRenderer, HighScoreLabel, HighScoreRenderer, GameOverLabel, GameOverHint; public Game() // . . . var LabelFont = new Font(new FontFamily(GenericFontFamilies.SansSerif), 20, GraphicsUnit.Pixel); var LabelColor = Color4.SteelBlue; NextStickLabel = new TextRenderer(LabelFont, LabelColor, "Next"); ScoreLabel = new TextRenderer(LabelFont, LabelColor, "Score"); HighScoreLabel = new TextRenderer(LabelFont, LabelColor, "High score"); var ScoreFont = new Font(new FontFamily(GenericFontFamilies.SansSerif), 50, GraphicsUnit.Pixel); var ScoreColor = Color4.Tomato; ScoreRenderer = new TextRenderer(ScoreFont, ScoreColor); HighScoreRenderer = new TextRenderer(ScoreFont, ScoreColor); var GameStateFont = new Font(new FontFamily(GenericFontFamilies.SansSerif), 30, GraphicsUnit.Pixel); var GameStateColor = Color4.Tomato; GameOverLabel = new TextRenderer(GameStateFont, GameStateColor, "Game over"); var GameStateHintFont = new Font(new FontFamily(GenericFontFamilies.SansSerif), 25, GraphicsUnit.Pixel); var GameStateHintColor = Color4.SteelBlue; GameOverHint = new TextRenderer(GameStateHintFont, GameStateHintColor, "Press Enter"); } protected override void OnRenderFrame(FrameEventArgs E) { // . . . GL.Translate(MapWidth * SolidSize + PipeMarginX, 0, 0); NextStickLabel.Render(); // TODO   next stick GL.Translate(0, MapHeight * SolidSize / 4f, 0); if (GameStateEnum.GameOver == GameState) { GameOverLabel.Render(); GL.Translate(0, GameOverLabel.Height, 0); GameOverHint.Render(); GL.Translate(0, -GameOverLabel.Height, 0); } GL.Translate(0, MapHeight * SolidSize / 4f, 0); ScoreLabel.Render(); GL.Translate(0, ScoreLabel.Height, 0); ScoreRenderer.Label = Score.ToString(); ScoreRenderer.Render(); GL.Translate(0, -ScoreLabel.Height, 0); GL.Translate(0, MapHeight * SolidSize / 4f, 0); HighScoreLabel.Render(); GL.Translate(0, HighScoreLabel.Height, 0); HighScoreRenderer.Label = HighScore.ToString(); HighScoreRenderer.Render(); SwapBuffers(); } 


MapHeight * SolidSize / 4f is a quarter of the height of the glass, each time we descend below this distance to depict one of the four elements of the interface. In addition, removing the inscription, we descend to its height below, and then do not forget to rise back to the starting point.

Commit: "Text GUI".

Next


Let's display the next stick itself. To begin, however, it is necessary to slightly change the model, because now we have the next stick generated at the moment of the start of the next move, but it is necessary that it be generated already on the current turn and stored somewhere.

 private int[] NextStickColors; private void GenerateNextStick() { for (var i = 0; i < StickLength; i++) { StickColors[i] = NextStickColors[i]; NextStickColors[i] = Rand.Next(ColorsCount); } StickPosition.X = (float)Math.Floor((MapWidth - StickLength) / 2d); StickPosition.Y = 0; } private void New() { // . . . StickColors = new int[StickLength]; NextStickColors = new int[StickLength]; GenerateNextStick(); GenerateNextStick(); // because 1st call makes current stick all zeros GameState = GameStateEnum.Fall; } 


To display, we use the RenderSolid method, everything is very simple.

 protected override void OnRenderFrame(FrameEventArgs E) { // . . . NextStickLabel.Render(); GL.Translate(0, NextStickLabel.Height, 0); RenderNextStick(); GL.Translate(0, -NextStickLabel.Height, 0); // . . . } public void RenderNextStick() { GL.Disable(EnableCap.Texture2D); GL.Color4(Color4.Black); GL.Begin(BeginMode.Quads); GL.Vertex2(0, 0); GL.Vertex2(StickLength * SolidSize, 0); GL.Vertex2(StickLength * SolidSize, SolidSize); GL.Vertex2(0, SolidSize); GL.End(); GL.Enable(EnableCap.Texture2D); for (var i = 0; i < StickLength; i++) { RenderSolid(i, 0, NextStickColors[i]); } } 


Done, commit: "Render next stick."

The score


Let's take the points. It is necessary to give more points for long lines, for the simultaneous destruction of several lines, for the sequential destruction of several lines within one turn. It will interest players to build complex combinations, will add interest to the game.

The formulas below are derived offhand, they, of course, will still need to be checked at the beta testing stage, to look at the player reviews.

 private int TotalDestroyedThisMove; private void New() { // . . . Score = 0; TotalDestroyedThisMove = 0; } protected override void OnUpdateFrame(FrameEventArgs E) { // . . . if (Destroyables.Count > 0) { foreach (var Coords in Destroyables) { Map[(int)Coords.X, (int)Coords.Y] = -1; } Score += (int)Math.Ceiling(Destroyables.Count + Math.Pow(1.5, Destroyables.Count - 3) - 1) + TotalDestroyedThisMove; TotalDestroyedThisMove += Destroyables.Count; Stabilized = false; } // . . . GenerateNextStick(); TotalDestroyedThisMove = 0; GameState = GameStateEnum.Fall; // . . . } 


At the end of the game, we will update the record (if it is broken) and write to the file, and when you start the application, read the current record from the file.

 using System.IO; // . . . private string HighScoreFilename; public Game() { // . . . var ConfigDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + "ImpressiveSolids"; if (!Directory.Exists(ConfigDirectory)) { Directory.CreateDirectory(ConfigDirectory); } HighScoreFilename = ConfigDirectory + Path.DirectorySeparatorChar + "HighScore.dat"; if (File.Exists(HighScoreFilename)) { using (var Stream = new FileStream(HighScoreFilename, FileMode.Open)) { using (var Reader = new BinaryReader(Stream)) { try { HighScore = Reader.ReadInt32(); } catch (IOException) { HighScore = 0; } } } } else { HighScore = 0; } } protected override void OnUpdateFrame(FrameEventArgs E) { // . . . if (GameOver) { GameState = GameStateEnum.GameOver; if (Score > HighScore) { HighScore = Score; using (var Stream = new FileStream(HighScoreFilename, FileMode.Create)) { using (var Writer = new BinaryWriter(Stream)) { Writer.Write(HighScore); } } } } else { // . . . } 


Commit: “Calculating score, storing high score”.

Heaven can wait


Finally, we will make it possible to pause the game.

A pause cannot be made as a state ( GameStateEnum ), because the game can be paused both during the fall of the stick ( Fall ) and during Impact , and from the pause the game should return to the state it was in.

Therefore, we introduce an additional Paused flag and its processing in OnUpdateFrame , OnKeyDown , OnRenderFrame .

 private bool Paused; private TextRenderer PauseLabel, UnpauseHint, PlayingGameLabel, PauseHint; public Game() // . . . var GameStateFont = new Font(new FontFamily(GenericFontFamilies.SansSerif), 30, GraphicsUnit.Pixel); var GameStateColor = Color4.Tomato; GameOverLabel = new TextRenderer(GameStateFont, GameStateColor, "Game over"); PauseLabel = new TextRenderer(GameStateFont, GameStateColor, "Pause"); PlayingGameLabel = new TextRenderer(GameStateFont, GameStateColor, "Playing"); var GameStateHintFont = new Font(new FontFamily(GenericFontFamilies.SansSerif), 25, GraphicsUnit.Pixel); var GameStateHintColor = Color4.SteelBlue; GameOverHint = new TextRenderer(GameStateHintFont, GameStateHintColor, "Press Enter"); UnpauseHint = new TextRenderer(GameStateHintFont, GameStateHintColor, "Press Space"); PauseHint = new TextRenderer(GameStateHintFont, GameStateHintColor, "Space pauses"); } protected override void OnLoad(EventArgs E) { base.OnLoad(E); GL.Enable(EnableCap.Texture2D); GL.Enable(EnableCap.Blend); GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha); New(); Paused = true; } protected override void OnUpdateFrame(FrameEventArgs E) { base.OnUpdateFrame(E); if (Paused) { return; } // . . . } protected void OnKeyDown(object Sender, KeyboardKeyEventArgs E) { if ((GameStateEnum.Fall == GameState) && !Paused) { // . . . } if (((GameStateEnum.Fall == GameState) || (GameStateEnum.Impact == GameState)) && (Key.Space == E.Key)) { Paused = !Paused; } } protected override void OnRenderFrame(FrameEventArgs E) { // . . . GL.Translate(0, MapHeight * SolidSize / 4f, 0); if (GameStateEnum.GameOver == GameState) { GameOverLabel.Render(); GL.Translate(0, GameOverLabel.Height, 0); GameOverHint.Render(); GL.Translate(0, -GameOverLabel.Height, 0); } else if (Paused) { PauseLabel.Render(); GL.Translate(0, PauseLabel.Height, 0); UnpauseHint.Render(); GL.Translate(0, -PauseLabel.Height, 0); } else { PlayingGameLabel.Render(); GL.Translate(0, PlayingGameLabel.Height, 0); PauseHint.Render(); GL.Translate(0, -PlayingGameLabel.Height, 0); } // . . . } 


A new game when starting the application is convenient (for the player) to start just in the pause state.

We use the spacebar, not the Pause key, because the spacebar is more convenient and always remembered about it, unlike Pause, which many people don’t even know about. In addition, problems arise with the latter, in particular, when using the Punto Switcher.

Commit: "Pause".

That's all for now. Sure, in the game you can still finish a lot of small nuances and particulars. Surely pop up bugs that need to be fixed. All this I leave to the reader for independent study.

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/134283/


All Articles