iOSでFootballoloidを書いたように

最も名誉あるHabralyudiへのご挨拶!





少し前、新しい仕事を探している間に、 ZeptoLabから非常に興味深いテストタスクを受け取りました。Cocos2dやBox2dなどのサードパーティライブラリを使用せずに、つまり「クリーンな」 OpenGLでiOS向けのアルカノイドを作成します 。それは私にとって非常に興味深いようでした。 ところで、Habréでのこのタスクについて 、すでに報告し 、さらに報告会を準備しました。 だから、私は運命に挑戦し、グラフィカルモードでVasikを使って学校で練習した後に初めてgamedevを取り上げました!



OpenGLの知識はすでにある程度ありましたが、非常に表面的なことを明確にします。 私たちはそれらがほとんどまったくなかったと言うことができます、私はビューポートとは何かを知っていて、そこにいくつかのスプライトがあります、変換マトリックスがあります...したがって、この記事は「 純粋なOpenGLでiOS用のシンプルなゲームを書く方法彼を知らずに、 「しかし、それは長すぎます。



一般的に、〜10時間の開発と〜2時間の読書でこれをどのように行ったかに興味がある場合は、猫をお願いします。 (注意!たくさんのコード!小さな写真!最後にgithubとリラックスビデオへのリンク!)



正直になります。 Image_to_attract_attention™は、私に起こったことのスクリーンショットではありません。 そして、これが私に起こったことです:





このごまかしを許してください、でもあなたは私の記事にまだ興味がありますよね? )



新しいテクノロジーを使用したプログラムの作成はどのように開始しますか? そうです、ドキュメント、例、記事を読むことから(まあ、あなたの目の前のようなものです)。 Googleに数分座った後、 素晴らしい記事が発見されました(著者Ray Wenderlich)。簡単なおもちゃを作成するプロセスを詳細に調べました。 ここで、必要に応じて、私の記事を閉じてRayの記事を読み始めることができますが、まだアルカノイドと母国語での説明のために来た人のために、話を続けます。



最初に、ゲーム用の空のプロジェクトを作成します。 Xcodeで、[ ファイル ] -> [新規 ] -> [プロジェクト... ]をクリックし、 iOSテンプレート->アプリケーション-> OpenGLゲームを選択します。 個人的には、ARCを使用し、StoryBoardを使用しないプロジェクトを選択しましたが、読者の皆さん、お好きなように自由に実行できます。 さて、プロジェクトが作成されました。すぐに[実行]を押して、回転するキューブを楽しむことができます。 しかし、これはまさに私たちがやりたいことではないので、テンプレートが挿入したほとんどすべてを削除し、必要なものだけを残します。 まず、追加されたシェーダーを削除し、ViewController.mからすべての列挙とinterface ViewController ()



向かうグローバル変数を削除しloadShaders, compileShader:type:file:, linkProgram:, validateProgram:



シェーダーは使用しません。 もちろん、方法と理由がわかっていれば使用できますが、私はこれを気にしませんでした=)。



次に、残りのすべてのメソッドから余分なものをすべて捨てます。 setupGL



tearDownGL



を見てみましょう。

 - (void)setupGL { [EAGLContextsetCurrentContext:self.context]; self.effect = [[GLKBaseEffectalloc] init]; } - (void)tearDownGL { [EAGLContextsetCurrentContext:self.context]; self.effect = nil; }
      
      





次。 プリミティブアーキテクチャでプリミティブゲームを作成するため、ViewControllerでゲーム全体を制御できます。 友好的な方法では、特定のGameControllerを作成し、これらすべての機能を割り当てる必要がありますが、今のところは管理します。 したがって、ViewControllerにそのようなプロパティとメソッドを追加します。



 #define kGameStateNone 0 #define kGameStateLose 1 #define kGameStateWon 2 @property (assign) int gameState; // see kGameState... @property (assign) BOOL gameRunning; - (void)loadBricks; - (void)startGame; - (void)endGameWithWin:(BOOL)win;
      
      





彼らが責任を負うもの-そして、コメントは明確ではありません。 さて、今がゲームのロジック、より正確にはプログラムのロジックについて考えるときです。 何をどのように行いますか? どのオブジェクトがありますか? これのために何をする必要がありますか?



たくさんの質問、はい。 しかし、単純な考え(およびいくつかのスプライトがあるという知識)を使用して、このモデルに到達します。プレーヤーが制御する「バット」があり、破壊する必要があるレンガがあり、すべてを跳ね返してレンガを破壊するボールがあります。 さらに、背景があり、メニューがあります-ゲームを起動して、その結果(勝ち負け)を確認できます。 画面に表示されるすべてのものは、システムのウィンドウなど、レンダリングの基本オブジェクトであるスプライトを使用して描画されます。 さて、またはウィンドウ内のボタン。 座標、サイズ、描画する画像など、さまざまな属性を設定できます。 今後、動きの速度と方向を追加します。 さて、これらのプロパティを用意し、Xcodeで⌘Nを大胆に押してください! NSObjectから継承した新しいクラスを作成して、GameSpriteと呼びましょう。 そして、そのようなプロパティとメソッドを追加します:



 @interface GameSprite : NSObject - (id)initWithTexture:(GLKTextureInfo *)textureInfo effect:(GLKBaseEffect *)effect; - (id)initWithImage:(UIImage *)image effect:(GLKBaseEffect *)effect; - (void)render; - (void)update:(float)dt; - (CGRect)boundingRect; @property (assign) GLKVector2 position; @property (assign) CGSize contentSize; @property (assign) GLKVector2 moveVelocity; // points/sec @end
      
      





実際には、画像またはテクスチャに基づいてスプライトを作成し、それらを描画し、時間に応じて更新し、境界線を取得することができます。 また、その位置、サイズ、速度を尋ねて取得します。 しかし、スプライトの実装では、楽しみが始まります! 頂点と四角形が必要です。 これは何ですか 単純な場合、頂点は空間内のポイントであり、クワッドは4つの頂点のコレクションです。 さらに、Vertextには、スプライト自体とそのテクスチャの2つのポイントが実際に含まれています。 対応する構造を宣言します。



 typedef struct { CGPoint geometryVertex; CGPoint textureVertex; } TexturedVertex; typedef struct { TexturedVertex bl; TexturedVertex br; TexturedVertex tl; TexturedVertex tr; } TexturedQuad;
      
      





この場合、次の機能を考慮する必要があります。テクスチャ座標は正規化されます。つまり、常に0〜1の範囲で変化します。スプライトでテクスチャを描画するにはこのクワッドが必要なので、GameSpriteクラスではこのようなプライベートプロパティを宣言します:



 @interface GameSprite() @property (strong) GLKBaseEffect *effect; @property (assign) TexturedQuad quad; @property (strong) GLKTextureInfo *textureInfo; - (void)initQuadAndSize; @end
      
      





したがって、効果があり、テクスチャ情報は同じ方法でプロパティに格納されますが、パブリックではありません。 さて、スプライトの初期化の実装を開始できます。



 - (id)initWithTexture:(GLKTextureInfo *)textureInfo effect:(GLKBaseEffect *)effect { if ((self = [super init])) { self.effect = effect; self.textureInfo = textureInfo; if (self.textureInfo == nil) { NSLog(@"Error loading texture! Texture info is nil!"); return nil; } [self initQuadAndSize]; } return self; } - (id)initWithImage:(UIImage *)image effect:(GLKBaseEffect *)effect { if ((self = [super init])) { self.effect = effect; NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], GLKTextureLoaderOriginBottomLeft, nil]; NSError *error; self.textureInfo = [GLKTextureLoader textureWithCGImage:image.CGImage options:options error:&error]; if (self.textureInfo == nil) { NSLog(@"Error loading image: %@", [error localizedDescription]); return nil; } [self initQuadAndSize]; } return self; } - (void)initQuadAndSize { self.contentSize = CGSizeMake(self.textureInfo.width, self.textureInfo.height); TexturedQuad newQuad; newQuad.bl.geometryVertex = CGPointMake(0, 0); newQuad.br.geometryVertex = CGPointMake(self.textureInfo.width, 0); newQuad.tl.geometryVertex = CGPointMake(0, self.textureInfo.height); newQuad.tr.geometryVertex = CGPointMake(self.textureInfo.width, self.textureInfo.height); newQuad.bl.textureVertex = CGPointMake(0, 0); newQuad.br.textureVertex = CGPointMake(1, 0); newQuad.tl.textureVertex = CGPointMake(0, 1); newQuad.tr.textureVertex = CGPointMake(1, 1); self.quad = newQuad; }
      
      





トリッキーなことはありません。画像からテクスチャを割り当てたり読み込んだり、クワッドを割り当てるだけです。 ここでは、4つの頂点のそれぞれについて、ジオメトリとテクスチャの座標が異なる単位で設定されていることがわかります。



それでは、最も興味深い部分に移りましょう-スプライトのレンダリングです。このためにrender



メソッドが責任を負います!



 - (void)render { self.effect.texture2d0.name = self.textureInfo.name; self.effect.texture2d0.enabled = YES; self.effect.transform.modelviewMatrix = self.modelMatrix; [self.effect prepareToDraw]; long offset = (long)&_quad; glEnableVertexAttribArray(GLKVertexAttribPosition); glEnableVertexAttribArray(GLKVertexAttribTexCoord0); glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE, sizeof(TexturedVertex), (void *) (offset + offsetof(TexturedVertex, geometryVertex))); glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(TexturedVertex), (void *) (offset + offsetof(TexturedVertex, textureVertex))); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); }
      
      





ここで、エフェクトを介してレンダリングするためのテクスチャを準備し(変換行列について-少し後で、この行を読むまで)、必要なOpenGLパラメーター(位置とテクスチャ)をオンにし、巧妙な変換を使用してクワッドをOpenGLに転送し、最後に描画します! すべては単純に思えますが、これらの行が行うことの詳細な説明はこの記事の範囲を超えています。読者のみなさん 、ドキュメンテーション( glVertexAttribPointerおよびglDrawArrays )にお送りします。



次に、座標、回転、スケーリングなどのすべてに責任を持つ変換マトリックスに目を向けます。 しかし、今のところは、スプライトを運動場の目的の部分に移動するだけでよいので、始めましょう。



 - (GLKMatrix4)modelMatrix { GLKMatrix4 modelMatrix = GLKMatrix4Identity; modelMatrix = GLKMatrix4Translate(modelMatrix, self.position.x, self.position.y, 0); modelMatrix = GLKMatrix4Translate(modelMatrix, -self.contentSize.width / 2, -self.contentSize.height / 2, 0); return modelMatrix; }
      
      





ここでも、すべてが簡単です。Identityマトリックス(何もしないIDマトリックス)を取得し、self.positionの座標の原点を変換してから、スプライトの中央に移動します。 これは、スプライトの束を操作するときに非常に便利です。 update:



用のスタブを作成したら、最初のスプライトを作成し、リソースから画像をロードできます。 そして、ViewControllerでそれを描きます:



 - (void)viewDidLoad { [super viewDidLoad]; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!self.context) { NSLog(@"Failed to create ES context"); } [self setupGL]; GLKView *view = (GLKView *)self.view; view.context = self.context; GLKMatrix4 projectionMatrix = GLKMatrix4MakeOrtho(0, 320, 0, 480, -1024, 1024); self.effect.transform.projectionMatrix = projectionMatrix; // initializing game state self.gameRunning = NO; self.gameState = kGameStateNone; // initializing sprites self.testSprite = [[GameSpritealloc] initWithImage:[UIImageimageNamed:@"myImage"] effect:self.effect]; self.testSprite .position = GLKVector2Make(160, 35); } - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(1.f, 1.f, 1.f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); [self.testSprite render]; }
      
      







まあ、悪くない? 現在、私たちの競技場に1つの絵が描かれています。 はい、私たちは友人に電話して、ほとんどゲームを書いたことを自慢できます! =)



しかし、実際には、喜ぶには時期尚早です。 まだ1つのスプライトがあり、移動しません。 とりあえず完全に削除して、たとえば、一般的に必要なスプライトを特定するなど、何か面白いことをします。 まず、少し。 第二に、ボール。 第三に、レンガ用のスプライトの束。 他に何か? ああ、もっと背景。 そしてまた-メニュー! そして、それを行う方法は? 暗くするためのスプライト、「勝った」碑文用のスプライト、「失われた」碑文用のスプライト、「井戸」という碑文用のスプライト、「再生」ボタン用のスプライト。 まあ、すべてがそうです、ふphe。 それでは、コントローラのプロパティを設定しましょう。



 @property (strong, nonatomic) GameSprite *playerBat; @property (strong, nonatomic) GameSprite *ball; @property (strong, nonatomic) GameSprite *background; @property (strong, nonatomic) GameSprite *menuDimmer; @property (strong, nonatomic) GameSprite *menuCaption; @property (strong, nonatomic) GameSprite *menuCaptionWon; @property (strong, nonatomic) GameSprite *menuCaptionLose; @property (strong, nonatomic) GameSprite *menuStartButton; @property (strong, nonatomic) NSMutableArray *bricks;
      
      







これらすべての要素について、gimp / photoshop / otherで適切なサイズの画像を作成します。 レンガのサイズを50x10ピクセルにしてみましょう。 ボールを引くこともできますし、ネット上で見つけることもできますが、実際にやってみました。 同志ジュニアは背景を手伝いました(そして、ちなみに、私は完全にサッカーのテーマを考えました)が、私はいくつかのレンガと少し自分自身を描きました。 すべてのスプライトの初期化コードを完全に提供するわけではありません。それはすでに与えられているものと同一であり、座標のみが異なります。 しかし、レンガをどうするか? それらの場所はどういうわけか、まったくハードコードになりたくないですよね? そして、まだレベルを下げたい場合は? これらのレベルが保存される独自のファイル形式を考えてみましょう。 私の形式は非常にシンプルになりましたが、想像力を自由に自由に発揮できます。 だから、ここに私の最初の(そして今のところ唯一の)レベルのファイルがあります:



 101101 111111 010010 111111 000000 111111
      
      





形式は自明のようですよね? 0-レンガなし、1-あり。 2番目のタイプのレンガを作成する場合は、2、3、4などの数字を入力します。 しかし、それは将来のために痛いです。 ブリックロード機能も非常に明確です。



 - (void)loadBricks { // assuming 6x6 brick matrix, each brick is 50x10 NSError *error; [NSBundle mainBundle] ; NSStringEncoding encoding; NSString *filePath = [[NSBundle mainBundle] pathForResource:@"level1" ofType:@"txt"]; NSString *levelData = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error]; if (levelData == nil) { NSLog(@"Error loading level data! %@", error); return; } levelData = [[levelData componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsJoinedByString: @""]; if ([levelData length] < (6*6)) { NSLog(@"Level data has incorrect size!"); return; } NSMutableArray *loadedBricks = [NSMutableArray array]; UIImage *brickImage = [UIImage imageNamed:@"brick1"]; NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], GLKTextureLoaderOriginBottomLeft, nil]; GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:brickImage.CGImage options:options error:&error]; if (textureInfo == nil) { NSLog(@"Error loading image: %@", [error localizedDescription]); return; } for (int i = 0; i < 6; i++) { for (int j = 0; j < 6; j++) { if ([levelData characterAtIndex:j + i * 6] == '1') { GameSprite *brickSprite = [[GameSprite alloc] initWithTexture:textureInfo effect:self.effect]; brickSprite.position = GLKVector2Make((j + 1) * 50.f - 15.f, 480.f - (i + 1) * 10.f - 15.f); [loadedBricks addObject:brickSprite]; } } } self.bricks = loadedBricks; }
      
      





さて、今もレンガを積みました。 あなたは私たちのゲームを実行し、レンガ、バット、ボール、そしてこれらすべてを背景の背景と照らし合わせて賞賛することができます。 そうそう、これは表示されませんglkView:drawInRect:



レンダリングを追加するのを忘れていglkView:drawInRect:



レンダリングを順番に追加します。最初に背景、次にビット、次にすべてのレンガ、最後にボールを追加します。 そして今、あなたは賞賛することができます! =)



しかし、どのように? ボールが動かないのはなぜですか? これは絵ではなく、これはゲームです。つまり、ボールはどこかに飛ばなければなりません! ボールを飛ばすには、速度を設定する必要があります。また、スプライトメソッドのupdate:



で、この速度を考慮して座標を変更する必要があります。



 - (void)update:(float)dt { GLKVector2 curMove = GLKVector2MultiplyScalar(self.moveVelocity, dt); self.position = GLKVector2Add(self.position, curMove); }
      
      





また、ViewControllerのupdate



メソッドでは、ボールのスプライトを更新する必要があります。



  [self.ball update:self.timeSinceLastUpdate];
      
      







これで、 startGame



ボールをゼロ以外の速度に設定できます。ボールが飛ぶようになります。



 - (void)startGame { self.gameRunning = YES; self.gameState = kGameStateNone; [selfloadBricks]; self.ball.position = GLKVector2Make(160, 80); self.ball.moveVelocity = GLKVector2Make(120, 240); }
      
      





さて、今度はviewDidLoad



このメソッドを呼び出しましょう-そしてボールは飛ぶでしょうが-すぐに画面から飛び出します。 うーん、悲しい! さて、衝突とボールと壁の衝突の処理を検討し始めます。 既製のupdate



方法で、ボールと壁の衝突を追加します。



 // checking for walls // left if (self.ball.boundingRect.origin.x <= 0) { self.ball.moveVelocity = GLKVector2Make(-self.ball.moveVelocity.x, self.ball.moveVelocity.y); self.ball.position = GLKVector2Make(self.ball.position.x - self.ball.boundingRect.origin.x, self.ball.position.y); } // right if (self.ball.boundingRect.origin.x + self.ball.boundingRect.size.width >= 320) { self.ball.moveVelocity = GLKVector2Make(-self.ball.moveVelocity.x, self.ball.moveVelocity.y); self.ball.position = GLKVector2Make(self.ball.position.x - (self.ball.boundingRect.size.width + self.ball.boundingRect.origin.x - 320), self.ball.position.y); } // top if (self.ball.boundingRect.origin.y + self.ball.boundingRect.size.height >= 480) { self.ball.moveVelocity = GLKVector2Make(self.ball.moveVelocity.x, -self.ball.moveVelocity.y); self.ball.position = GLKVector2Make(self.ball.position.x, self.ball.position.y - (self.ball.boundingRect.origin.y + self.ball.boundingRect.size.height - 480)); } // bottom (player lose) if (self.ball.boundingRect.origin.y + self.ball.boundingRect.size.height <= 70) { [self endGameWithWin:NO]; }
      
      





ロジックは単純で、ティゴノメトリーさえ必要ありません。左右の壁と衝突すると、速度の水平成分を反転させ、上部の壁と垂直成分を反転させます。 入射角は反射角と同じです。つまり、物理法則が守られています。 さて、下の境界を越えるとき、損失を数えます。 ああ。 ちなみに、ボールの位置を「修正」して、プラグを回避します。



発射! ボールは右の壁から上から跳ね返りました-そして、私たちはそれを知りませんでしたが、負けました。 もう悪くない! ここで、バットから跳ね返る必要があります。アルカノイドで慣習的であるように、入射角は常に反射角と等しくなるとは限りません。これは、ボールがバットのどこに落ちたかによって異なります。 このまさに角度を計算するために、三角法はすでにここで適用可能です:



  // player strikes! if (CGRectIntersectsRect(self.ball.boundingRect, self.playerBat.boundingRect)) { float angleCoef = (self.ball.position.x - self.playerBat.position.x) / (self.playerBat.contentSize.width / 2); float newAngle = 90.f - angleCoef * 80.f; GLKVector2 ballDirection = GLKVector2Normalize(GLKVector2Make(1 / tanf(GLKMathDegreesToRadians(newAngle)), 1)); float ballSpeed = GLKVector2Length(self.ball.moveVelocity); self.ball.moveVelocity = GLKVector2MultiplyScalar(ballDirection, ballSpeed); self.ball.position = GLKVector2Make(self.ball.position.x, self.ball.position.y + (self.playerBat.boundingRect.origin.y + self.playerBat.boundingRect.size.height - self.ball.boundingRect.origin.y)); }
      
      





十分にプリミティブ。 しかし今、楽しい部分が始まります。レンガとの衝突の定義、それらの破壊、ボールのリバウンド。



  // checking for broken bricks NSMutableArray *brokenBricks = [NSMutableArray array]; GLKVector2 initialBallVelocity = self.ball.moveVelocity; for (GameSprite *brick in self.bricks) { if (CGRectIntersectsRect(self.ball.boundingRect, brick.boundingRect)) { [brokenBricks addObject: brick]; if ((self.ball.position.y < brick.position.y - brick.contentSize.height / 2) || (self.ball.position.y > brick.position.y + brick.contentSize.height / 2)) { self.ball.moveVelocity = GLKVector2Make(initialBallVelocity.x, -initialBallVelocity.y); } else { self.ball.moveVelocity = GLKVector2Make(-initialBallVelocity.x, initialBallVelocity.y); } } } // removing them for (GameSprite *brick in brokenBricks) { [self.bricks removeObject:brick]; } if (self.bricks.count == 0) { [self endGameWithWin:YES]; }
      
      





ここで、ロジックはより巧妙です:どちらの側でボールがブリックに当たったかを判断し、これに応じて速度の必要なコンポーネントを変更し、ブリックに壊れたマークを付けます。 さて、壊れたものをすべて削除し、残っているものがなければゲームを終了し、勝ちました!



さて、ゲームを開始して、ボールがどのようにジャンプし、レンガを壊し、...を見ることができます。 コウモリがいますが、それを制御する必要がありますか? 私はコウモリを制御する最も簡単な方法を選択しました。「耳」にその下の場所を割り当て、親指でドラッグできます。 ビットの位置を変更するには、指を押したり動かしたりするイベントを受け取る必要があります。そのためには、ジェスチャー認識機能が必要です。 それらをインストールします。



 - (void)viewDidLoad { // ... // gestures UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGestureFrom:)]; [self.view addGestureRecognizer:panRecognizer]; [self.view addGestureRecognizer:tapRecognizer]; } - (void)handleTapGestureFrom:(UITapGestureRecognizer *)recognizer { CGPoint touchLocation = [recognizer locationInView:recognizer.view]; if (self.gameRunning) { GLKVector2 target = GLKVector2Make(touchLocation.x, self.playerBat.position.y); self.playerBat.position = target; } } - (void)handlePanGesture:(UIGestureRecognizer *)gestureRecognizer { CGPoint touchLocation = [gestureRecognizer locationInView:gestureRecognizer.view]; if (self.gameRunning) { GLKVector2 target = GLKVector2Make(touchLocation.x, self.playerBat.position.y); self.playerBat.position = target; } }
      
      





さて、あなたは遊ぶ準備ができていますか? 立ち上げ、すべてのレンガを壊しました! それでは、次は何ですか? プレーヤーに自分の労働の結果を示す必要があり、したがってメニューを表示する必要があります。 通常、GameSpriteから継承されたGameSceneクラスを使用して実装されたこのようなもののために別々のシーンが作成されますが、コードをできるだけ単純にすることは避けました。 したがって、メニューは異なるスプライトから組み立てられます。 まあ、 glkView:drawInRect:



ゲームの状態を確認し、必要なすべてをレンダリングする必要があります。



 - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(1.f, 1.f, 1.f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); [self.background render]; [self.playerBat render]; for (GameSprite *brick in self.bricks) { [brick render]; } [self.ball render]; if (!self.gameRunning) { [self.menuDimmer render]; [self.menuStartButton render]; switch (self.gameState) { case kGameStateWon: [self.menuCaptionWon render]; break; case kGameStateLose: [self.menuCaptionLose render]; break; case kGameStateNone: default: [self.menuCaption render]; break; } } }
      
      





ここでも、すべてが非常にシンプルであり、コメントしたくありません。 「play」ボタンのクリックを処理するために残ります。これは、 handleTapGestureFrom:



に「else」ブロックをhandleTapGestureFrom:



します。



  else if (CGRectContainsPoint(self.menuStartButton.boundingRect, touchLocation)) { [self startGame]; }
      
      





それだけです! , , ! — ! , , OpenGL!





, . , , , . ( , ? , )



, . , , , , , . , , Game Center, iCloud, Facebook/Twitter, , . App Store, , .



PS : . ZeptoLab, OpenGL . : , , . , , ! , , .



All Articles