週末に空のペットボトル、写真、シェーダーからiOSゲームを作成する方法

妻に尋ねたら:

「週末の予定はありませんか?」

「そうではないようだ」と彼女は答えた。

「それから、このSwiftをもう一度選びます。」

-突く。



そして、私は、Swiftで非常に簡単なiOSおもちゃを書くタスクを自分自身に設定しましたが、 ^。* C. * $ (Swiftでの私の最後の経験は、プロジェクトの80% Objective-C(私のC ++の考え方により、私が知っている最も近いもの(Objective-C)+ 2C-Objective = C)に削減されました)。



挑戦する



ダン: 1枚の写真、私の頭の中にある考え。

それは必要です:ゲームは月曜日のアラームの前に書かれました。



私にとって痛みのないものについては詳しく説明しませんが、あなたにも誤解を招かないように願っています。



OpenGLを使用するためのエンティティ



なぜ私が純粋なOpenGLで書いているのか、またSpriteKitを使わないのかを聞かないでください。私自身はこの質問に対する答えを知りません。

そこで、プロジェクトを作成し、メインのストーリーボードのエディターを開き、ここですべてを削除しました。 GLKViewControllerをボードにドラッグし、GameViewControllerクラスとそのビューを割り当てました-GameView:



import UIKit import GLKit class GameView: GLKView { override func drawRect(rect: CGRect) { glClearColor(0.8, 0.4, 0.2, 1.0) glClear(GLbitfield(GL_COLOR_BUFFER_BIT)) } }
      
      





glClear呼び出しに注意してください。この関数は、GLbitfield型(UInt32)の引数を取ります。 ああ、なんて不運なのでしょう。定数はInt32型のGL_COLOR_BUFFER_BITであり、Swiftでは暗黙的なキャストは禁止されているからです。 この事実は、最初は私を怒らせましたが、この壮大な禁止により、不注意なプログラマーのコード(もちろん、あなたは気配りがあり、何も必要ありません)が少し良くなることに気付きました。



Win + R⌘+ Rを押しますが、何が見えますか? いいえ、オレンジ色の画面ではなく、白です。 OpenGLESコンテキストを初期化するのを忘れたためです。



 class GameViewController: GLKViewController { var gameView: GameView! override func viewDidLoad() { super.viewDidLoad() gameView = self.view as GameView gameView.context = EAGLContext(API: .OpenGLES3) assert(gameView.context != nil, "Cannot to initialize OpenGL ES3.") } }
      
      





私たちの知識なしでこれを行うことを妨げ、ストーリーボードエディタでOpenGLESのバージョンを示すことだけを求めているように思われますか? 私たちの頬を吹き飛ばさないようにしましょう。GLKitの開発者にはこれには十分な理由があると思います。



次に、テクスチャをロードしてみましょう。 このために、私はこのクラスを始めました:



テクスチャクラス
 class Texture { let name: GLuint = 0 let width: GLsizei let height: GLsizei init( image: UIImage, wrapX: GLint = GL_CLAMP_TO_EDGE, wrapY: GLint = GL_CLAMP_TO_EDGE, filter: GLint = GL_NEAREST ) { let cgImage = image.CGImage width = GLsizei(CGImageGetWidth(cgImage)); height = GLsizei(CGImageGetHeight(cgImage)); let pixelCount = width * height var imageData = [UInt32](count: Int(pixelCount), repeatedValue:0) let imageContext = CGBitmapContextCreate( &imageData, UInt(width), UInt(height), 8, UInt(width * 4), CGImageGetColorSpace(cgImage), CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.rawValue) ) CGContextDrawImage(imageContext, CGRect(x: 0, y: 0, width: Int(width), height: Int(height)), cgImage); glGenTextures(1, &name); glBindTexture(GLenum(GL_TEXTURE_2D), name); glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), filter); glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), filter); glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), wrapX); glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), wrapY); glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), imageData); } deinit { glDeleteTextures(1, [name]) } }
      
      





初期化の簡単なエッセンス:UIImage(汎用画像)を取得し、それをCGIImage( 処理用の画像 )に変換し、CGContext(使用可能なメモリを備えた描画コンテキスト-imageData)を作成し、このコンテキストで画像を描画し、次に送信しますglTexImage2Dを使用したビデオカードメモリ。 おそらくいくつか質問がありますか?



  1. なんてこった! なぜこれほど多くのステップがあるのですか? UIImageからgetDataを取得できませんか?
  2. これらのGLenum(..)を見ると痛いです。 なぜスウィフトはそんなに残酷なのですか?


私はおそらくいくつかの答えがありました:



  1. いいえ、できません。 そしてこれは、画像のバイトにアクセスできる変換の最短チェーンであり、標準関数を使用して見事にできました。
  2. いくつかのアップデートで、AppleはこれらのCコードのSwiftへの自動変換を修正し、関数の引数のタイプは実際の引数のタイプと一致し始めると思いますが、今のところはそれを許容します。


ほんの一握りの構文糖:変数の前のアンパサンドがUnsafeMutablePointer(SwiftでC'shポインターを操作するためのクラス)に変わります。 そして、配列はUnsafePointer型にキャストされます(暗黙的なキャスト、検出!)。



私は助けることができませんが、シェーダークラスをもたらす:



クラスシェーダー
 class Shader { let handle: GLuint init(name: String, type: GLint) { let file = NSBundle.mainBundle().pathForResource(name, ofType: (type == GL_VERTEX_SHADER ? "vert" : "frag"))! let source = NSString(contentsOfFile: file, encoding: NSUTF8StringEncoding, error: nil)! var sourceCString = source.UTF8String var sourceLength = GLint(source.length) handle = glCreateShader(GLenum(type)) glShaderSource(handle, 1, &sourceCString, &sourceLength) glCompileShader(handle); var compileSuccess = GLint(-42) glGetShaderiv(handle, GLenum(GL_COMPILE_STATUS), &compileSuccess) if (compileSuccess == GL_FALSE) { var log = [GLchar](count:1024, repeatedValue: 0) glGetShaderInfoLog(handle, GLsizei(log.count), nil, &log) NSLog("\nShader '\(name)' is wrong: [\n\(NSString(bytes: log, length: log.count, encoding: NSUTF8StringEncoding)!)\n]") } } deinit { glDeleteShader(handle) } }
      
      





興味深いことは何もありません。C関数でC以外のオブジェクトを突き止めるもう1つの試みです。



シェーダー



画面全体に2つの三角形を描画して、画面上の各ピクセルに対してフラグメントシェーダーを実行し、小さな奇跡を作成しましょう。

クレイジーペンが始まります! 誰も必要としない古い頂点シェーダーを使用してください:



 attribute vec2 vertex; varying vec2 coord = vertex; void main(void) { gl_Position = vec4(vertex, 0., 1.); }
      
      





空のペットボトルを(真剣に?)取り出し、その底を切り取り、じょうごに目を通します。これがゲームの外観です。 穏やかに、今、私はすべてを説明します。次のように配置された平面上に正方形の等角写像を取ります。







そして、トンネルを移動しているように変化させてみてください。 フラグメントシェーダーのスケッチをしましょう。



 uniform sampler2D img; varying vec2 coord; float pi2 = 6.2832; void main() { float r = length(coord); float a = atan( coord.y , coord.x ); vec2 uv = vec2(a/pi2, r); gl_FragColor = texture2D(img, uv); }
      
      





はい、画像の直交座標を極座標に変換しただけで、目的を達成しましたが、目的は達成していません。



極座標の画像






事実、無限のトンネルを想像して、それに沿って見ると、トンネルの終わりが見えないということです。 彼はどこまでも遠い中心にいる…ちょっと待って! 無限にリモートですか? あなたはおそらく、トリッキーなデバイスなしで自宅で無限大を取得する方法を知っていますか? もちろん! 画面の中央で、ゼロで除算します。



 vec2 uv = vec2(a/pi2, 1./r);
      
      





トンネル






トンネルを少し歩いてみましょう。



 gl_FragColor = texture2D(img, uv + uv(0, pos.z));
      
      





変数pos.zは時間とともに増加し、トンネルに沿って前進します。 次に、スクリーンプレーンに動きを追加します。



 vec2 newCoord = coord + pos.xy;
      
      





それが起こった






ある種のナンセンスが出てきた、私たちは描かれた絵を動かした。 遠近法の効果を達成してみましょう。トンネルの中にいると想像してください。 次に、右に移動し始めると、中央の右側のすべてが平らになり、左側のすべてが伸びます。



 float r = length(newCoord) - dot(newCoord, pos);
      
      





悪魔、何をしたの? はい、今半径があります-バービーサイズ!



奇妙な半径






そして、なぜこの変換が必要なことをするのでしょうか? ドットは、2つのベクトルのスカラー積をカウントする関数です。



 dot(newCoord, pos) = newCoord.x * pos.x + newCoord.y * pos.y
      
      





最初の項を考えてみましょう:半径pos.xと座標newCoord.xが同じ符号を持っている場合(newCoord.xがシフトpos.xの方向でトンネルの中心の後ろにある場合)、半径から正の値が減算されます。この方向は縮小され、平坦化された画像です。 pos.xとnewCoord.xの符号が異なる場合、ストレッチが発生します。 同様のことがY座標でも起こります。 これは正直な見通しではありませんが、非常に高速と見なされます。 そして、わずかな逸脱で、詐欺はほとんど見えません。

次に、風景を変更します。 私はすでにこれらのコンクリートの壁を見るのにうんざりしています。



景色の変化






高速走行中にぼかしを追加します。



 float ds = speed / blurCount; for (float dx = -speed; dx < speed; dx += ds) gl_FragColor += texture2D(img, uv + vec2(0, pos.z + dx)); gl_FragColor /= gl_FragColor.a;
      
      





移動方向に沿ってシフトされたいくつかの画像を平均し、高速ライドの効果を得ました。



ブリューユユル!






信号機を追加します。



 gl_FragColor += texture2D(img, uv + vec2(0, pos.z + dx)) + lightColor / distance(uv, lightPos);
      
      





光源からの距離と逆の色を追加しただけです。



それが起こったことです






それはおそらくグラフィックスですべてです。



ゲームサイクル



ゲームロジックを実装するには、GameViewをGLKViewControllerのデリゲートにします。



 class GameView: GLKView, GLKViewControllerDelegate { /*...*/ }
      
      





ストーリーボードエディターで、GLKViewControllerを右クリックし、デリゲートフィールドをGLKViewに接続します。 これで、GameViewでglkViewControllerUpdateメソッドを定義すると、各フレームをレンダリングする前に呼び出されます。 その中で、各フレームにゲームロジックを実装しました。複数のゲーム状態(加速、ブレーキ、一時停止)を入力し、各状態に対して、移動速度とカメラの位置の動作を説明しました。 赤信号の通過に反応した。 遅すぎる運転時に損失を追加しました。



全部



マイナーな修正の後、ゲームをAppStoreに投稿しました。 ゲームの公開プロセスは、作成よりも時間がかかりました。すべてのプラットフォームのスクリーンショットを撮り、説明とタグを追加し、Appleの承認を待ちます(ああ、ホラー、9日間!)。



Swiftには、私のメイン言語と比べて多くの便利な機能があります。 iOSの開発はとても楽しいように思えました。 このトピックに関する新しい記事をお待ちください。



All Articles