印象的なソリッド:OpenGLのC#でゲームを作成する、パートII

装飾されたゲームのスクリーシンショト テトリスのようなゲームImpressive Solids 開発の最初の部分では、アプリケーションの外観に最小限の注意を払いながら、ゲームプレイの大部分を実装しました。 OpenGLもほとんど使用しませんでした。色付きの四角形を描画するだけで使用しました。 スコアリングを実装し、ハイスコアを維持するだけでなく、設計に参加する時が来ました。 さて、さらに進んでみましょう。



この画像



テクスチャに注意しましょう。 まず、ウィンドウの背景に何かを引っ張り、次に見栄えの良いブロックを作成する必要があります(現在、これらは単なる色付きの長方形です)。 ビジネスをクリアするには、最初にテクスチャを作成する必要があります。 GIMPはこれに役立ちます。 グラフィックスを使用したくない場合は、既製のテクスチャを含むアーカイブをダウンロードして、次の段階に進むことができます



しかし、最初に、非常に重要なニュアンスに注目します。 OpenGL 2.0より前は、各テクスチャサイズは2の累乗に等しくなければなりませんでした(つまり、64×64、512×256。これらは英語の2の累乗からのPOTテクスチャです)。 任意のサイズのテクスチャ(NPOTテクスチャ)がビデオカードまたはビデオカードドライバでサポートされていない場合、そのようなテクスチャは機能しません。 これは、たとえば、Windows XPでのIntel統合ビデオカードの場合です。



この問題からあなたが安全であることを確実にするために、最も簡単で最も便利な解決策は、常にPOTテクスチャを使用することです。 ただし、これは常に可能とは限りません。さらに、テキストの結論に達すると、この瞬間に取り組む必要があります。



そこで、GIMPで空の(白い)画像512×512を作成し、フィルター→アーティスティック→キャンバスを適用してから、フィルター→マップ→シームレスに作成します。 すべて、background.pngの準備ができました。



ブロックを大理石のボールとして描写しようとします。 ダニエル・ケッチャムはこれを手伝ってくれます。 300×300のどこかに透明な画像を作成し、キャンバスの直径全体を丸く選択します。 ツールバケット塗りつぶし→パターン塗りつぶし→テクスチャ大理石#1を選択し、円を塗りつぶします 次:フィルター→歪み→レンズの歪み、メインを最大に、エッジを最小に、OKを選択します。 次に、フィルター→光と影→照明効果、3次元のボールの効果を作成するように光を設定します。 空のフィールドがないように切り抜きます。 256×256のサイズに拡大縮小します。 次に、Colors→Colorizeを使用して、5つの異なる色を作成し(色相をオンにし)、0.png、1.png ... 4.pngとして保存します(ゲームモデルでは、ブロックの異なる色をゼロから始まる整数として指定することにしました)。



次に、これらのファイルをVisual C#Express / MonoDevelopプロジェクトに取り込む必要があります。 最初に、コンテキストメニューからtexturesという名前の新しいフォルダーを作成し、その中にソリッドを作成します。 ファイルマネージャーを介して、background.pngファイルをテクスチャに配置し、0.png ... 4.pngファイルをテクスチャ/ソリッドに配置します。 開発環境のコンテキストメニューを使用して、これらのファイルを適切なフォルダーのプロジェクトに追加します。



その後、プロジェクト内のすべての* .pngファイルについて、プロパティを開き、ビルドアクション:コンテンツを設定します。 出力ディレクトリにコピー:新しい場合はコピーします。



OpenGLでテクスチャを有効にします。 OpenGL自体はグラフィックファイルでは動作しないため、独自の手段( System.Drawing.Bitmap



が役立ちます)を使用してテクスチャをメモリにロードし、そこからバイナリビットマップを取得し、OpenGLに転送して、メモリにテクスチャを保存します。 将来的には、整数ハンドル(最初に予約する必要があります)を介してテクスチャにアクセスできます。



新しい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 } }
      
      







Game



、テクスチャをロードします。



 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")); } }
      
      







レンダリングを変更します。 一般にテクスチャと透過モードを含める必要があります。 次に、長方形の座標を設定する前に、対応するテクスチャを選択(バインド)する必要があります。 各ポイント(頂点)の前に、テクスチャの対応する座標を設定する必要があります((1; 1)はテクスチャの右下隅であると考えられています)。



 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(); }
      
      







与えられた色、つまり色はテクスチャの色調を整えるため、白色を指定します。



素晴らしい、テクスチャが機能します。 コミット:「テクスチャ背景とソリッド」。



メインストリート



次に、ガラスを識別する必要があります。 ウィンドウの背景上で、ブロックの背後にある単なる黒い長方形にします。



 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(); // . . . }
      
      







ガラスを配置します。 ウィンドウの端からわずかにくぼみを付けて、左側にします。 後でガラスの右側に、追加のインターフェイス要素(アカウントなど)を配置します。 ただし、ウィンドウの幅を広げる場合は、ガラスをウィンドウの中央に向かって右に延ばします。そうしないと、右側に空きスペースが多くなりすぎます。 最後に、ウィンドウをNominalWidth



× NominalHeight



より小さくNominalWidth



ことをNominalWidth



します(ただし、これはXウィンドウシステムでは機能しません)。



 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(); // . . . }
      
      







コミット:「位置とパイプのレンダリング」。



壁に書く



ガラスの右側には何がありますか? 4つの要素:次のスティック。 ゲームのステータス(「Playing」、「Paused」、または「Game Over」というメッセージが表示されます); 現在のアカウント。 記録スコア。



このほとんどはテキストなので、OpenGLを使用してテキストを出力する方法を学ぶ必要があります。 OpenGL自体はサポートしていません。 多くの場合、OpenTKにはこの目的に便利なTextPrinter



クラスがあるという事実への参照があります。 それはずっと前のことで、真実ではありませんでした。 現在、テキスト表示するため推奨方法は次のとおりです。テキストを使用してビットマップを作成し( System.Drawing.Graphics.DrawString



またはその他の手段を使用)、テクスチャのようにそれを引き出します。



独自のTextRenderer



クラスを作成して、 Bitmap



を作成し、それに基づいてTexture



を作成しましょう。 ただし、最初に、前述のNPOT次元テクスチャの問題を心配する必要があります。これは、動的に作成された碑文がどのサイズになるかが事前にわからないためです。 この方法は非常に簡単です。NPOTテクスチャがサポートされていない場合、写真を読み込むときにフィールドのようにPOTサイズのテクスチャを作成する必要があります。 たとえば、300×200の画像をアップロードすると、画像が左上隅にある512×256のテクスチャが生成され、残りのスペースは空になります。 また、テクスチャを適用する場合、画像の左下隅の座標が(1; 1)ではなく(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); // . . . } // . . . }
      
      







TextRenderer



。 コードは長いように見えますが、本当に簡単です。 ここでは、4つの基本的なことを行います。テキストパラメータを設定し、サイズを測定し、テキストをテクスチャに描画し、最後に透明な長方形にテクスチャを適用します。



 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(); } } }
      
      







ガラスの右側に何かを引き出します。



 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



は、ガラスの高さの4分の1であり、この距離を下るたびに4つのインターフェイス要素の1つを表します。 さらに、碑文を表示して、その高さまで降りてから、出発点に戻ることを忘れないでください。



コミット:「テキストGUI」。



次へ



実際に次のスティックを表示します。 ただし、最初に、次の動きの開始時に次のスティックが生成されるため、モデルをわずかに変更する必要がありますが、現在の動きで既に生成され、どこかに保存されている必要があります。



 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; }
      
      







表示するには、 RenderSolid



メソッドを使用します。すべてが非常に簡単です。



 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]); } }
      
      







完了、コミット:「次の棒をレンダリング」。



スコア



採点しましょう。 長い線、複数の線の同時破壊、1回の動きで複数の線を一貫して破壊するには、より多くのポイントを与える必要があります。 これにより、プレイヤーは複雑な組み合わせを構築し、ゲームに興味を持たせることができます。



以下の数式は、手っ取り早く表示されます。もちろん、プレイヤーのレビューを見るには、ベータテストの段階でそれらを確認する必要があります。



 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; // . . . }
      
      







ゲームの終わりに、レコードを更新し(破損している場合)、ファイルに書き込み、アプリケーションの起動時に、ファイルから現在のレコードを読み取ります。



 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 { // . . . }
      
      







コミット:「スコアの計算、ハイスコアの保存」。



天国は待つことができます



最後に、ゲームを一時停止できるようにします。



状態( GameStateEnum



)として一時停止することはできません。スティックの落下( Fall



)とImpact



両方の間にゲームを一時停止でき、一時停止からゲームは元の状態に戻るはずです。



したがって、追加のPaused



フラグとその処理を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); } // . . . }
      
      







アプリケーションを開始するとき、一時停止したときに新しいゲームを開始すると便利です(プレイヤーにとって)。



一時停止キーではなくスペースを使用します。これは、多くの人が知らない一時停止とは異なり、スペースがより便利で常に記憶されるためです。 さらに、後者には、特にPunto Switcherを使用する場合に問題があります。



コミット:「一時停止」。



今のところすべてです。 間違いなく、ゲームではまだ多くの小さなニュアンスや詳細を仕上げることができます。 修正が必要なバグがポップアップ表示されます。 これらすべては、独立した研究のために読者に任せます。



このプロジェクトは Googleプロジェクトホスティング Bitbucketでは、最終的なソースコードを確認し、実行可能な実行可能ファイルを含むアーカイブをダウンロードできます。



All Articles