Once I asked my wife:
- We have no plans for the weekend?
βIt seems not,β she replied.
- Then I once more dig this Swift.
- Dig up.
So I set myself the task of writing a very simple toy for iOS on Swift, without resorting to any ^. * C. * $ (my previous experience with Swift ended up with 80% of the project on Objective-C (which, due to my C ++ thinking, was reduced to the nearest (Objective-C) + 2C-Objective = C) known to me.Task
Given: One picture, some thoughts in my head.
Necessary: ββA game written before the alarm bell on Monday.
')
I will not describe in detail things that have been painless for me, I hope that they should not cause any misunderstandings among you.
Entities for working with OpenGL
Do not ask why I am writing on pure OpenGL, and not using any SpriteKit, I myself do not know the answer to this question.
So, I created a project, opened the editor of the main Storyboard, deleted everything here. Dragged to the GLKViewController board, assigned him a GameViewController class, its view - 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)) } }
Note the glClear call: this function takes an argument of type GLbitfield (UInt32). Oh, what a bad luck, because the constant is GL_COLOR_BUFFER_BIT of type Int32, and implicit type conversion is forbidden in Swift. This fact, at first grieved me, but then I realized that this magnificent ban causes the code of an inattentive programmer (yes, you are of course attentive and you need nothing) to be a little better.
Click
Win + R β + R, and what do we see? No, not the orange screen is white. This is because we forgot to initialize the OpenGLES context:
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.") } }
It would seem that it was impossible to do this without our knowledge, and ask us only to indicate the version of OpenGLES in the storyboard editor? Let's not inflate the cheeks: I think the GLKit developers had good reasons for this.
Let's now try to load the texture. For this, I started this class:
Texture class 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]) } }
Brief essence of initialization: take a UIImage (general purpose image), convert it into a CGIImage (image for
processing ), create a CGContext (drawing context with an accessible memory space for us - imageData), draw our image in this context, and then send it to video memory with glTexImage2D. You probably have a couple of questions?
- Oh my Globe! Why so many steps? Can't u get get from UIImage?
- It hurts me to look at these GLenum (..). Why is Swift so cruel?
I probably had a couple of answers:
- No, it is impossible. And this is the shortest chain of transformations after which you can access the bytes of the image, which I was able to make using standard functions.
- I think in some update Apple will correct these automatic conversions of C's code to Swift and the types of function arguments will start to agree with the types of real arguments, but for now we are tolerant.
A small handful of syntactic sugar: an ampersand in front of a variable turns it into an UnsafeMutablePointer (a class for working with C pointers in Swift); and the arrays are cast (implicit casting, yet, detected!) to the UnsafePointer type.
I can not cite the class shader:
Shader class 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) } }
There is nothing interesting in it, just another attempt to push through not C's objects in C's functions.
Shaders
Let's draw two triangles all over the screen so that for each pixel on the screen a fragmentary shader will be executed that will create a small miracle.
Crazy hands begin! Take the old vertex shader that nobody wants:
attribute vec2 vertex; varying vec2 coord = vertex; void main(void) { gl_Position = vec4(vertex, 0., 1.); }
Now we take an empty plastic bottle (seriously?), Cut off its bottom and look into the funnel - this is how our game will look like. Calmly, now I will explain everything: we take a conformal mapping of a square onto a plane arranged as follows:

And try to vary it as if we are moving through the tunnel. Make a sketch for a fragment shader:
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); }
Yes, we just transferred the rectangular coordinates of the image to the polar ones and already achieved what we wanted, but not what we had in mind.
The fact is that if you imagine an endless tunnel and look along it, then you will never see the end of the tunnel. It is somewhere in the infinitely remote center ... Wait a minute! Infinitely remote? You probably know how to get infinity at home without any tricky devices? Of course! In the center of the screen we divide by zero:
vec2 uv = vec2(a/pi2, 1./r)
Now let's take a little walk through the tunnel:
gl_FragColor = texture2D(img, uv + uv(0, pos.z))
The variable pos.z increases over time, and we run along the tunnel ahead. Now add the movement in the screen plane:
vec2 newCoord = coord + pos.xy;
Some nonsense came out, we just moved the drawing image. Let's try to achieve the effect of perspective: imagine that you are inside a tunnel. Now start moving to the right then, all that to the right of the center will begin to flatten, and all that to the left will stretch:
float r = length(newCoord) - dot(newCoord, pos)
What have you done, demon? Yes, I have a radius now - barbie-size!
And why does this transformation do what we need? dot is a function that considers the scalar product of two vectors, i.e.
dot(newCoord, pos) = newCoord.x * pos.x + newCoord.y * pos.y
Let's look at the first addend: if the deviation pos.x and the coordinate newCoord.x, for which we assume the radius, have the same sign (it means newCoord.x is lagging behind the center of the tunnel in the direction of pos.x shift) then a positive value is subtracted from the radius, this direction decreases and flattens the image; when pos.x and newCoord.x have a different sign, stretching occurs. The same happens with the y-coordinates. Although not an honest perspective, it is considered very fast; and at small deviations the deception is almost not noticeable.
Now we change the situation. I'm already tired of looking at these concrete walls.
Add blur when driving at high speed:
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;
We have just averaged several images shifted along the direction of movement, and got the effect of fast driving:
Add traffic lights:
gl_FragColor += texture2D(img, uv + vec2(0, pos.z + dx)) + lightColor / distance(uv, lightPos)
Just add some color in inverse proportion to the distance from the light source.
On this, perhaps, with graphics - everything.
Game cycle
In order to implement the game logic, we will make the GameView delegate to the GLKViewController:
class GameView: GLKView, GLKViewControllerDelegate { }
In the storyboard editor, right-click GLKViewController and connect the delegate field to GLKView. Now, if you define the glkViewControllerUpdate method in GameView, it will be called before each frame is drawn. In it, I implemented the game logic for each frame: I started several game states (acceleration, braking, pause) and for each state I described the behavior of the speed of movement and camera position; made a reaction to the passage of red light; added a loss when driving too slow.
Everything
After minor improvements, I posted the game on the
AppStore . The process of publishing the game took more time than creating it: taking screenshots for all platforms, adding descriptions and tags, waiting for approval from Apple (oh, horror, 9 days!).
Swift has many convenient features compared to my main language; iOS development seemed to me quite enjoyable. So wait for new articles on this topic.