GLKit Tutorial for iOS: Getting started with OpenGL ES

Update note: This tutorial has been updated to Swift 4.1 and Xcode 9.3 by Felipe Laso-Marsetti. The original tutorial was written by Ray Wenderlich.

GLKit Tutorial for iOS: Getting started with OpenGL ES

If you’re interested in graphics programming, chances are that you’ve read about OpenGL, which remains the most-adopted API from a hardware and software perspective. Apple has developed a framework called GLKit to help developers create apps that leverage OpenGL and to abstract boilerplate code. It also allows developers to focus on drawing, not on getting the project set up. You’ll learn how all of this works in this GLKit tutorial for iOS.

GLKit provides functionality in four areas:

  • Views and View Controllers: These abstract much of the boilerplate code that GLKit uses to set up a basic OpenGL ES (Embedded Systems) project.
  • Effects: These implement common shading behaviors and are a handy way of setting up basic lighting, shading, reflection mapping and skybox effects.
  • Math: Provides helpers and functions for common math routines like vector and matrix manipulation.
  • Texture Loading: Makes it much easier to load images as textures to be used in OpenGL.
Note: You will use OpenGL ES 3.0, which is available on iPhone 5S and above, iPad Mini 2 and above, and iPad 5th generation and above.

Without further ado, it’s time to get started!

Getting Started

The goal of this tutorial is to get you up-to-speed with the basics of using OpenGL with GLKit, assuming you have no previous experience with this whatsoever. You will build an app that draws a cube to the screen and makes it rotate.

There’s no starter project for this tutorial. You’re going to make it all from scratch!

Open Xcode and create a brand new project. Select the iOS\Application\Single View App template.

Set the Product Name to OpenGLKit and the Language to Swift. Make sure none of the checkboxes are selected. Click Next, choose a folder in which to save your project and click Create.

Build and run. You’ll see a simple, blank screen:

Introducing GLKView and GLKViewController

Here’s where the fun begins! Open ViewController.swift and replace its contents with:

import GLKit

class ViewController: GLKViewController {

}

You need to import GLKit and your view controller needs to be subclass of GLKViewController.

GLKit is supported in Interface Builder, so this is the best way to set it up. Do that now.

Open Main.storyboard and delete the contents of the storyboard. Then, from the Object Library, drag a GLKit View Controller into your scene.

In the Identity inspector, change the class to ViewController. In the Attributes inspector, select the Is Initial View Controller checkbox.

Finally, change the Preferred FPS to 60:

With the Attributes inspector open, click the GLKView in the canvas and notice some of the settings for color, depth and stencil formats, as well as for multisampling. You only need to change these if you’re doing something advanced, which you’re not here. So the defaults are fine for this tutorial.

Your OpenGL context has a buffer that it uses to store the colors that will be displayed to the screen. You can use the Color Format property to set the color format for each pixel in the buffer.

The default value is GLKViewDrawableColorFormatRGBA8888, meaning that eight bits are used for each color component in the buffer (four total bytes per pixel). This is optimal because it gives you the widest possible range of colors to work with which means the app will look more high quality.

That’s all the setup you need to do in the storyboard. Your view controller is set up with a GLKView to draw OpenGL content into, and it’s also set as the GLKViewDelegate for your update and draw calls.

Back in ViewController.swift, add the following variable and method:

private var context: EAGLContext?

private func setupGL() {
  // 1
  context = EAGLContext(api: .openGLES3)
  // 2
  EAGLContext.setCurrent(context)

  if let view = self.view as? GLKView, let context = context { 
    // 3
    view.context = context
   // 4 
    delegate = self
  }
}

Here’s what’s happening in this method:

  1. To do anything with OpenGL, you need to create an EAGLContext.
  2. An EAGLContext manages all of the information that iOS needs to draw with OpenGL. It’s similar to needing Core Graphics context to do anything with Core Graphics. When you create a context, you specify what version of the API that you want to use. In this case, you want to use OpenGL ES 3.0.

  3. Specifies that the rendering context that you just created is the one to use in the current thread.
  4. OpenGL contexts should not be shared across threads, so you will have to make sure that you only interact with this context from whichever thread you used to call this method `setupGL()`.

  5. This sets the GLKView’s context. After unwrapping the necessary variables, you set the GLKView‘s context to this OpenGL ES 3.0 context that you created.
  6. This sets the current class (ViewController) as the GLKViewController’s delegate. Whenever state and logic updates need to occur, the glkViewControllerUpdate(_ controller:) method will get called.

Having done this, add the following to implement viewDidLoad() to call this method:

override func viewDidLoad() {
  super.viewDidLoad()
  setupGL()
}

So now you know which thread called `setupGL()` — it’s the main thread, which is the special thread that is dedicated to interactions with UIKit and that is used by the system when it calls `viewDidLoad()`.

At this point, you may notice that there’s an error. This is because you’re not conforming to GLKViewControllerDelegate yet. Go ahead and make it conform by adding the following extension:

extension ViewController: GLKViewControllerDelegate {
  func glkViewControllerUpdate(_ controller: GLKViewController) {

  }
}

Next, add the following method to the ViewController main class definition:

override func glkView(_ view: GLKView, drawIn rect: CGRect) {
  // 1
  glClearColor(0.85, 0.85, 0.85, 1.0) 
  // 2
  glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
}

This is part of the GLKViewDelegate, which draws contents on every frame. Here’s what it does:

  1. Calls glClearColor to specify the RGB and alpha (transparency) values to use when clearing the screen. You set it to a light gray, here.
  2. Calls glClear to actually perform the clearing. There can be different types of buffers like the render/color buffer you’re displaying right now, and others like the depth or stencil buffers. Here you use the GL_COLOR_BUFFER_BIT flag to specify that you want to clear the current render/color buffer.

Build and run the app. Notice how the screen color has changed:

Creating Vertex Data for a Simple Square

It’s time to begin the process of drawing a square on the screen! Firstly, you need to create the vertices that define the square. Vertices (plural of vertex) are simply points that define the outline of the shape that you want to draw.

You will set up the vertices as follows:

Only triangle geometry can be rendered using OpenGL. You can, however, create a square with two triangles as you can see in the picture above: One triangle with vertices (0, 1, 2) and one triangle with vertices (2, 3, 0).

One of the nice things about OpenGL ES is that you can keep your vertex data organized however you like. For this project, you will use a Swift structure to store the vertex position and color information, and then an array of vertices for each one that you’ll use to draw.

Right click the OpenGLKit folder in the Project navigator and select New File… Go to iOS\Swift File and click Next. Name the file Vertex and click Create. Replace the contents of the file with the following:

import GLKit

struct Vertex {
  var x: GLfloat
  var y: GLfloat
  var z: GLfloat
  var r: GLfloat
  var g: GLfloat
  var b: GLfloat
  var a: GLfloat
}

This is a pretty straightforward Swift structure for a vertex that has variables for position (x, y, z) and color (r, g, b, a). GLFloat is a type alias for a Swift Float, but it’s the recommended way to declare floats when working with OpenGL. You may see similar patterns wherein you use OpenGL types for other variables that you create.

Return to ViewController.swift. Add the following code inside your controller:

var Vertices = [
  Vertex(x:  1, y: -1, z: 0, r: 1, g: 0, b: 0, a: 1),
  Vertex(x:  1, y:  1, z: 0, r: 0, g: 1, b: 0, a: 1),
  Vertex(x: -1, y:  1, z: 0, r: 0, g: 0, b: 1, a: 1),
  Vertex(x: -1, y: -1, z: 0, r: 0, g: 0, b: 0, a: 1),
]

var Indices: [GLubyte] = [
  0, 1, 2,
  2, 3, 0
]

Here, you are using the Vertex structure to create an array of vertices for drawing. Then, you create an array of GLubyte values. GLubyte is just a type alias for good old UInt8, and this array specifies the order in which to draw each of the three vertices that make up a triangle. That is, the first three integers (0, 1, 2) indicate to draw the first triangle by using the 0th, the 1st and, finally, the 2nd verex. The second three integers (2, 3, 0) indicate to draw the second triangle by using the 2nd, the 3rd and then the 0th vertex.

Because triangles share vertices, this saves resources: You create just one array with all of the four vertices, and then you use a separate array to define triangles by referring to those vertices. Because an array index that points to a vertex takes less memory than the vertex itself, this saves memory.

With this complete, you have all the information you need to pass to OpenGL to draw your square.

Creating Vertex Buffer Objects and a Vertex Array Object

The best way to send data to OpenGL is through something called Vertex Buffer Objects. These are OpenGL objects that store buffers of vertex data for you.

There are three types of objects to be aware of, here:

  • Vertex Buffer Object (VBO): Keeps track of the per-vertex data itself, like the data you have in the Vertices array.
  • Element Buffer Object (EBO): Keeps track of the indices that define triangles, like the indices you have stored in the Indices array.
  • Vertex Array Object (VAO): This object can be bound like the vertex buffer object. Any future vertex attribute calls you make — after binding a vertex array object — will be stored inside it. What this means is that you only have to make calls to configure vertex attribute pointers once and then — whenever you want to draw an object — you bind the corresponding VAO. This facilitates and speeds up drawing different vertex data with different configurations.

At the top of ViewController.swift, add the following Array extension to help getting the size, in bytes, of the Vertices and Indices arrays:

extension Array {
  func size() -> Int {
    return MemoryLayout<Element>.stride * self.count
  }
}

An important subtlety here is that, in order to determine the memory occupied by an array, we need to add up the stride, not the size, of its constituent elements. An element’s stride is, by definition, the amount of memory the element occupies when it is in an array. This can be larger than the element’s size because of padding, which is basically a technical term for “extra memory that we use up to keep the CPU happy.”

Next, add the following variables inside ViewController:

private var ebo = GLuint()
private var vbo = GLuint()
private var vao = GLuint()

These are variables for the element buffer object, the vertex buffer object and the vertex array object. All are of type GLuint, a type alias for UInt32.

Setting Up the Buffers

Now, you want to start generating and binding buffers, passing data to them so that OpenGL knows how to draw your square on screen. Start by adding the following helper variables at the bottom of the setupGL() method:

// 1
let vertexAttribColor = GLuint(GLKVertexAttrib.color.rawValue)
// 2
let vertexAttribPosition = GLuint(GLKVertexAttrib.position.rawValue)
// 3    
let vertexSize = MemoryLayout<Vertex>.stride
// 4
let colorOffset = MemoryLayout<GLfloat>.stride * 3
// 5
let colorOffsetPointer = UnsafeRawPointer(bitPattern: colorOffset)

Here’s what that does:

  1. When you generate your buffers, you will need to specify information about how to read colors and positions from your data structures. OpenGL expects a GLuint for the color vertex attribute. Here, you use the GLKVertexAttrib enum to get the color attribute as a raw GLint. You then cast it to GLuint — what the OpenGL method calls expect — and store it for use in this method.
  2. As with the color vertex attribute, you want to avoid having to write that long code to store and read the position attribute as a GLuint.
  3. Here, you take advantage of the MemoryLayout enum to get the stride, which is the size, in bytes, of an item of type Vertex when in an array.
  4. To get the memory offset of the variables corresponding to a vertex color, you use the MemoryLayout enum once again except, this time, you specify that you want the stride of a GLfloat multiplied by three. This corresponds to the x, y and z variables in the Vertex structure.
  5. Finally, you need to convert the offset into the required type: UnsafeRawPointer.

With some helper constants ready, it’s time for you to create your buffers and set them up via a VAO for drawing.

Creating VAO Buffers

Add the following code right after the constants that you added inside setupGL():

// 1
glGenVertexArraysOES(1, &vao)
// 2
glBindVertexArrayOES(vao)

The first line asks OpenGL to generate, or create, a new VAO. The method expects two parameters: The first one is the number of VAOs to generate — in this case one — while the second expects a pointer to a GLuint wherein it will store the ID of the generated object.

In the second line, you are telling OpenGL to bind the VAO you that created and stored in the vao variable and that any upcoming calls to configure vertex attribute pointers should be stored in this VAO. OpenGL will use your VAO until you unbind it or bind a different one before making draw calls.

Using VAOs adds a little bit more code, but it will save you tons of time by not having to write lines of code to everything needed to draw even the simplest geometry.

Having created and bound the VAO, it’s time to create and set up the VBO.

Creating VBO Buffers

Continue by adding this code at the end of setupGL():

glGenBuffers(1, &vbo)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), vbo)
glBufferData(GLenum(GL_ARRAY_BUFFER), // 1
             Vertices.size(),         // 2
             Vertices,                // 3
             GLenum(GL_STATIC_DRAW))  // 4

Like the VAO, glGenBuffers tells OpenGL you want to generate one VBO and store its identifier in the vbo variable.

Having created the VBO, you now bind it as the current one in the call to glBindBuffer. The method to bind a buffer expects the buffer type and buffer identifier. GL_ARRAY_BUFFER is used to specify that you are binding a vertex buffer and, because it expects a value of type GLenum, you cast it to one.

The call to glBufferData is where you’re passing all your vertex information to OpenGL. There are four parameters that this method expects:

  1. Indicates to what buffer you are passing data.
  2. Specifies the size, in bytes, of the data. In this case, you use the size() helper method on Array that you wrote earlier.
  3. The actual data you are going to use.
  4. Tells OpenGL how you want the GPU to manage the data. In this case, you use GL_STATIC_DRAW because the data you are passing to the graphics card will rarely change, if at all. This allows OpenGL to further optimize for a given scenario.

By now, you may have noticed that working with OpenGL in Swift has a pattern of having to cast certain variables or parameters to OpenGL-specific types. These are type aliases and nothing for you to be worried about. It makes your code a bit longer or trickier to read at first, but it’s not difficult to understand once you get into the flow of things.

You have now passed the color and position data for all your vertices to the GPU. But you still need to tell OpenGL how to interpret that data when you ask it to draw it all on screen. To do that, add this code at the end of setupGL():

glEnableVertexAttribArray(vertexAttribPosition)
glVertexAttribPointer(vertexAttribPosition,       // 1 
                      3,                          // 2
                      GLenum(GL_FLOAT),           // 3
                      GLboolean(UInt8(GL_FALSE)), // 4 
                      GLsizei(vertexSize),        // 5
                      nil)                        // 6
    
glEnableVertexAttribArray(vertexAttribColor)
glVertexAttribPointer(vertexAttribColor, 
                      4, 
                      GLenum(GL_FLOAT), 
                      GLboolean(UInt8(GL_FALSE)), 
                      GLsizei(vertexSize), 
                      colorOffsetPointer)

You see another set of very similar method calls. Here’s what each does, along with the parameters they take. Before you can tell OpenGL to interpret your data, you need to tell it what it’s even interpreting in the first place.

The call to glEnableVertexAttribArray enables the vertex attribute for position so that, in the next line of code, OpenGL knows that this data is for the position of your geometry.

glVertexAttribPointer takes six parameters so that OpenGL understands your data. This is what each parameter does:

  1. Specifies the attribute name to set. You use the constants that you set up earlier in the method.
  2. Specifies how many values are present for each vertex. If you look back up at the Vertex struct, you’ll see that, for the position, there are three GLfloat (x, y, z) and, for the color, there are four GLfloat (r, g, b, a).
  3. Specifies the type of each value, which is float for both position and color.
  4. Specifies if you want the data to be normalized. This is almost always set to false.
  5. The size of the stride, which is a fancy way of saying “the size of the data structure containing the per-vertex data, when it’s in an array.” You pass vertexSize, here.
  6. The offset of the position data. The position data is at the very start of the Vertices array, which is why this value is nil.

The second set of calls to glEnableVertexttribArray and glVertexAttribPointer are identical except that you specify that there are four components for color (r, g, b, a), and you pass a pointer for the offset of the color memory of each vertex in the Vertices array.

With your VBO and its data ready, it’s time to tell OpenGL about your indices by using the EBO. This will tell OpenGL what vertices to draw and in what order.

Creating EBO Buffers

Add the following code at the bottom of setupGL():

glGenBuffers(1, &ebo)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), ebo)
glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER), 
             Indices.size(), 
             Indices, 
             GLenum(GL_STATIC_DRAW))

This code should look familiar to you. It’s identical to what you used for the VBO. You first generate a buffer and store its identifier in the ebo variable, then you bind this buffer to the GL_ELEMENT_ARRAY_BUFFER, and, finally, you pass the Indices array data to the buffer.

The last bit of code to add to this method is the following lines:

glBindVertexArrayOES(0)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0)
glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), 0)

First, you unbind (detach) the VAO so that any further calls to set up buffers, attribute pointers, or something else, is not done on this VAO. The same is done for the vertex and element buffer objects. While not necessary, unbinding is a good practice and can help you avoid logic bugs in the future by not associating setup and configuration to the wrong object.

Build your project to make sure it compiles, then press ahead.

Tidying Up

You’ve created several buffers that need to be cleaned up. Add the following method in ViewController to do so:

private func tearDownGL() {
  EAGLContext.setCurrent(context)
    
  glDeleteBuffers(1, &vao)
  glDeleteBuffers(1, &vbo)
  glDeleteBuffers(1, &ebo)
        
  EAGLContext.setCurrent(nil)
        
  context = nil
}

With the code above, you:

  • Set the EAGLContext to your context — the one you’ve been working with this whole time.
  • Delete the VBO, EBO and VAO.
  • Nil out the context and also set the context variable to nil to prevent anything else from being done with it.

Now, add the following method:

deinit {
  tearDownGL()
}

This is the deinitializer, which simply calls the teardown method.

Build and run the project — notice the same gray screen? The thing about graphics programming and working with OpenGL is that it often requires a lot of initial setup code before you can see things.

Introducing GLKBaseEffect

Now it’s time for the next topic: Shaders.

Modern OpenGL uses what’s known as a programmable pipeline that gives developers full control of how each pixel is rendered. This gives you amazing flexibility and allows for some gorgeous scenes and effects to be rendered. The tradeoff, however, is that there’s more work for the programmer than in the past. Shaders are written in GLSL (OpenGL Shading Language) and need to be compiled before they can be used.

Here’s where GLKit comes to the rescue! With GLKBaseEffect, you don’t have to worry about writing shaders. It helps you achieve basic lighting and shading effects with little code.

To create an effect, add this variable to ViewController:

private var effect = GLKBaseEffect()

Then, add this line at the bottom of glkView(_ view:, drawIn rect:):

effect.prepareToDraw()

That single line of code binds and compiles shaders for you, and it does it all behind the scenes without writing any GLSL or OpenGL code. Pretty cool, huh? Build your project to ensure it compiles.

With your buffers and effects ready, you now need three more lines of code to tell OpenGL what to draw and how to draw it. Add the following lines right below the line you just added:

glBindVertexArrayOES(vao);
glDrawElements(GLenum(GL_TRIANGLES),     // 1
               GLsizei(Indices.count),   // 2
               GLenum(GL_UNSIGNED_BYTE), // 3
               nil)                      // 4
glBindVertexArrayOES(0)

Here’s what each method call does. The call to glBindVertexArrayOES binds (attaches) your VAO so that OpenGL uses it — and all its the setup and configuration — for the upcoming draw calls.

glDrawElements() is the call to perform drawing and it takes four parameters. Here’s what each of them does:

  1. This tells OpenGL what you want to draw. Here, you specify triangles by using the GL_TRIANGLES parameter cast as a GLenum.
  2. Tells OpenGL how many vertices you want to draw. It’s cast to GLsizei since this is what the method expects.
  3. Specifies the type of values contained in each index. You use GL_UNSIGNED_BYTE because Indices is an array of GLubyte elements.
  4. Specifies an offset within a buffer. Since you’re using an EBO, this value is nil.

The moment of truth has arrived! Time to build and run your project.

You’ll see something like this:

Not bad, but also not what you were expecting. This isn’t a square being drawn and it’s not rotating. What’s going on? Well, no properties were set on the GLKBaseEffect, specifically, the transform properties for the projection and model view matrices.

Setting Up the Geometry

Time for some theory…

A projection matrix is how you tell the GPU how to render 3D geometry on a 2D plane. Think of it as drawing a bunch of lines out from your eye through each pixel in your screen. The pixel that is drawn to the screen is determined by whatever the frontmost 3D object each line hits.

GLKit has some handy functions to set up a projection matrix. The one you’re going to use allows you to specify the field of view along the y-axis, the aspect ratio and the near and far planes.

The field of view is like a camera lens. A small field of view (e.g., 10) is like a telephoto lens — it magnifies images by “pulling” them closer to you. A large field of view (e.g., 100) is like a wide-angle lens — it makes everything seem farther away. A typical value to use for this is around 65-75.

The aspect ratio is the aspect ratio that you want to render to (e.g., the aspect ratio of the view). It uses this in combination with the field of view, which is for the y-axis, to determine the field of view along the x-axis.

The near and far planes are the bounding boxes for the “viewable” volume in the scene. If something is closer to the eye than the near plane, or further away than the far plane, it won’t be rendered.

Note: This is a common problem to run into – you try and render something, which doesn’t show up. One thing to check is that the object is actually between the near and far planes.

Add the following code to the bottom of glkViewControllerUpdate(_:):

// 1
let aspect = fabsf(Float(view.bounds.size.width) / Float(view.bounds.size.height))
// 2
let projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0), aspect, 4.0, 10.0)
// 3
effect.transform.projectionMatrix = projectionMatrix

Here’s what that does:

  1. Calculates the aspect ratio of the GLKView.
  2. Uses a built-in helper function in the GLKit math library to create a perspective matrix; all you have to do is pass in the parameters discussed above. You set the near plane to four units away from the eye, and the far plane to 10 units away.
  3. Sets the projection matrix on the effect’s transform property.

You need to set one more property on the effect — the modelViewMatrix. This is the transform that is applied to any geometry that the effect renders.

The GLKit math library, once again, comes to the rescue with some handy functions that make performing translations, rotations and scales easy, even if you don’t know much about matrix math. Add the following lines to the bottom of glkViewControllerUpdate(_ controller:):

// 1
var modelViewMatrix = GLKMatrix4MakeTranslation(0.0, 0.0, -6.0)
// 2
rotation += 90 * Float(timeSinceLastUpdate)
modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, GLKMathDegreesToRadians(rotation), 0, 0, 1)
// 3
effect.transform.modelviewMatrix = modelViewMatrix

If you look back to where you set up the vertices for the square, remember that the z-coordinate for each vertex was 0. If you tried to render it with this perspective matrix, it wouldn’t show up because it’s closer to the eye than the near plane.

Here’s how you fix that with the code above:

  1. The first thing you need to do is move the objects backwards. In the first line, you use the GLKMatrix4MakeTranslation function to create a matrix that translates six units backwards.
  2. Next, you want to make the cube rotate. You increment an instance variable, which you’ll add in a second, that keeps track of the current rotation and use the GLKMatrix4Rotate method to change the current transformation by rotating it as well. It takes radians, so you use the GLKMathDegreesToRadians method for the conversion.
  3. Finally, you set the model view matrix on the effect’s transform property.

Finally, add the following property to the top of the class:

private var rotation: Float = 0.0

Build and run the app one last time and check out the results:

A rotating square! Perfect!

Where to Go From Here?

Congratulations! You’ve made your very own OpenGL ES 3.0 app with GLKit from the ground up. You can download the final project using the Download Materials link at the top or bottom of this tutorial.

You’ve learned about important concepts and techniques like vertex and element buffers, vertex attributes, vertex array objects and transformations. There are many effects you can still do with GLKBaseEffect for reflection maps, lighting, fog and more, as well as using the texture-loading classes to apply textures to your geometry.

I hope you enjoyed this tutorial, and if you have any questions or comments about OpenGL or GLKit, please join the discussion in our forums below. Happy rendering! :]

Download Materials

Team

Each tutorial at www.raywenderlich.com is created by a team of dedicated developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Felipe Laso-Marsetti

Felipe Laso is a Tech Lead working at Lextech Global Services. He’s also an aspiring game designer/programmer. You can follow him on Twitter as @iFeliLM or on his blog.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 27 total!

iOS Team

... 83 total!

Android Team

... 47 total!

Unity Team

... 16 total!

Articles Team

... 4 total!

Resident Authors Team

... 32 total!

Podcast Team

... 4 total!

Recruitment Team

... 8 total!

Illustration Team

... 4 total!