As I wrote Footballoloid under iOS

  • Tutorial
Greetings to the most honorable Habralyudi!


Not so long ago, while searching for a new job, I received a very interesting test task from ZeptoLab : to write an arkanoid for iOS in a couple of working days without using third-party libraries like Cocos2d / Box2d, etc., that is, on a “clean” OpenGL , which seemed very interesting to me. By the way, about this task on Habré already wrote , and even arranged debriefing . So, I challenged fate and took up gamedev for the first time after school exercises with Vasik in graphic mode!

I’ll clarify that I already had some knowledge of OpenGL, but very, very superficial. We can say that they weren’t almost completely, I just knew what a viewport is and there are some sprites there, that there are transformation matrices ... So this article could be entitled " How to write a simple game for iOS on pure OpenGL without knowing him, "but it's too long.

In general, if you are interested in how I did this for ~ 10 hours of development and ~ 2 hours of reading, I ask for cat. (Caution! A lot of code! Little pictures! Link to github and relaxing video at the end!)

We will be honest. Image_to_attract_attention ™ is not a screenshot of what happened to me. And here's what happened to me:


Forgive me for this deception, but you are still interested in my article, right? )

How does writing a program using technologies new to you begin? That's right, from reading documentation, examples and articles (well, like the one in front of you). After a couple of minutes of sitting on Google, a wonderful article was discovered (author Ray Wenderlich), which examined in detail the process of creating a simple toy. Here, if you wish, you can close my article and start reading Ray's article, but for those who still came for an arkanoid and explanations in their native language, I will continue the story.

First, create an empty project for the game. In Xcode, click File -> New -> Project ... , select the iOS template -> Application -> OpenGL Game. Personally, I chose a project with ARC and without StoryBoard, but you, dear reader, are free to do as you like. Well, the project is created, we can now immediately press Run and enjoy the spinning cubes. But this is not exactly what we wanted to do, so we delete almost everything that the template inserted for us, we leave only the necessary. First of all, we remove the added shaders, from ViewController.m we delete all the enums and global variables that go to. interface ViewController ()Now we remove the extra methods:, loadShaders, compileShader:type:file:, linkProgram:, validateProgram:in our simplest example, we will not use shaders. Of course, you can use them if you know how and why, but I did not bother with this =).

Next, we throw out all the excess from all the remaining methods. setupGLand tearDownGLbring to mind:
- (void)setupGL
{
    [EAGLContextsetCurrentContext:self.context];
    self.effect = [[GLKBaseEffectalloc] init];
}
- (void)tearDownGL
{
    [EAGLContextsetCurrentContext:self.context];
    self.effect = nil;
}

Further. We make a primitive game with a primitive architecture, so let our ViewController control the whole game. In an amicable way, it would be necessary to create a certain GameController and assign all these functions to it, but for now we will manage and so. Therefore, we add such properties and methods to our 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;

What they are responsible for - and no comment is clear. Well, now is the time to think about the logic of the game, more precisely, about the logic of the program. What and how will we do? What objects will we have? What do you need to do for this?

A lot of questions, yes. But just thinking (and knowing that there are some sprites) we come to this model: there is a “bat” that the player controls, there are bricks that need to be broken and there is a ball that bounces off everything and breaks the bricks. In addition, there is a background, and there is a menu - where we can launch the game and see its results (we won or lost). Everything that we see on the screen is drawn using sprites, which are the basic objects for rendering, something like windows in the system. Well, or the buttons in the window. They can have various attributes: coordinates, sizes, the picture they draw. Looking ahead, we add here more speed and direction of movement. Well, arm ourselves with these properties and boldly press ⌘N in Xcode! Create a new class inherited from NSObject , call it GameSprite. And add to it such properties and methods:

@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

Actually, we can create sprites based on a picture or texture, draw them, update depending on the time, get its borders. And also ask and get its position, size and speed. But in the implementation of the sprite, the fun begins! We need vertexes and quads. What it is? Well, if simple, then a vertex is a point in space, and a quad is a collection of four vertexes. Moreover, the vertex actually contains two points - for the sprite itself and for its texture. Declare the corresponding structures:

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

In this case, the following feature should be taken into account: the texture coordinates are normalized, that is, they always change in the range from 0 to 1. We need this quad to draw our texture with a sprite, so for the GameSprite class we will declare such private properties:

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

Thus, we have the effect and texture information will be stored in the same way in the properties, but not public. Well, now you can start implementing sprite initialization:

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

Nothing tricky, just assigning or loading textures from a picture and assigning a quad. Here you can see that for each of the four vertices the coordinates of the geometry and texture are set in different units.

Now let's move on to the most interesting part - drawing a sprite, for which the method is responsible 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);
}

Here we prepare the texture for rendering through the effect (about the transformation matrix - a little later, until you read this line), turn on the necessary OpenGL parameters (position and texture), transfer our quad to OpenGL with the help of cunning transformations, and finally draw! Everything seems to be simple, but a detailed explanation of what these lines do is beyond the scope of this article, I will just send you, dear reader, to the documentation ( glVertexAttribPointer and glDrawArrays ).

We now turn to the transformation matrix, which will be responsible for everything: for coordinates, rotation, scaling, and other things. But, for now, we only need to move our sprite to the desired part of the playing field, so let's go:

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

Everything here is also simple: take the Identity matrix (the identity matrix, which does nothing), translate the origin of coordinates in self.position, and then move it to the middle of the sprite. This is very convenient when manipulating a bunch of sprites. Now, having made a stub for update:, we can create our first sprite and load a picture from resources into it. And in our ViewController, draw it:

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


Well, not bad? One picture is now being drawn on our playing field. Yes, we can call friends and brag about almost writing the game! =)

But, in fact, it is too early to rejoice. We still have one sprite and it does not move. We’ll remove it for now and do something interesting, for example, we’ll figure out what kind of sprites we generally need. First of all, a bit. Secondly, the ball. Thirdly, a bunch of sprites for bricks. Something else? Oh yes, more background. And also - the menu! And how to do it? A sprite for darkening, a sprite for the inscription “won”, a sprite for the inscription “lost”, a sprite for the inscription “well”, a sprite for the “play” button. Well, everything seems to be, fuh. Well, let's go, set the properties for our controller:

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


For all these elements, create images of suitable sizes in the gimp / photoshop / other. Let the bricks have 50x10 pixels, a very good size. You can draw a ball, or you can find it on the net, which I actually did. Comrade JuniorI helped with the background (yes, by the way, and I came up with a completely football theme), but I drew the bricks and the bat myself somehow. I will not give completely the initialization code for all sprites, it is identical to the one already given, only the coordinates differ. But what to do with bricks? Their location somehow does not want to be hardcode at all, right? And if we still want to cut the level? Let's better come up with our own file format in which these levels will be stored. My format came out very simple, but you are free to give free rein to imagination. So, here is my file for the first (and so far the only) level:

101101
111111
010010
111111
000000
111111

The format seems to be self-evident, right? 0 - no brick, 1 - there is. Well, if you want to make the second type of bricks, then we enter the numbers 2, 3, 4 and so on. But it is, hurt for the future. The brick loading function is also very clear:

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

Well, now we loaded the bricks too. You can run our game and admire the even rows of bricks, our bat and ball, and all this against the background of our background. Oh yes, we won’t see this, we forgot to add rendering to glkView:drawInRect:! Add the rendering in order: first the background, then the bits, then all the bricks, and at the end - the ball. And now you can admire! =)

But how so? Why doesn't the ball move? This is not a picture, this is a game, which means that the ball must fly somewhere! In order for the ball to fly, he needs to set the speed, and in the sprite method update:you need to take this speed into account and change the coordinates:

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

And in the method of updateour ViewController, you need to update the sprite of the ball:

     [self.ball update:self.timeSinceLastUpdate];


Now you can set the ball to a non-zero speed in startGame- and the ball will fly!

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

Well, now let's call this method in viewDidLoad- and the ball will fly, but - quickly fly off the screen. Hmm, sad! Well, we begin to think about handling collisions and collisions of the ball with the walls. In the ready-made method, updateadd the collision of the ball with the walls:

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

The logic is simple, even tigonometry is not required: when we collide with the left and right walls, we invert the horizontal component of speed, and with the upper wall, vertical. The angle of incidence is equal to the angle of reflection - which means the laws of physics are observed. Well, when crossing the lower border, we count the loss. Alas. By the way, we also “correct” the position of the ball to avoid plugs.

Launch! The ball bounced off the right wall, from the top - and we lost, although we did not know about it. Already not bad! Now we need to bounce off the bat, and, as is customary in Arkanoid, the angle of incidence will not always be equal to the angle of reflection, which will depend on where the ball fell on the bat. Trigonometry is already applicable here to calculate this very angle:

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

Primitive enough. But now the fun part begins: the definition of a collision with bricks, their destruction and the rebound of the ball.

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

Here the logic is more cunning: we determine on which side the ball hit the brick, and depending on this we change the necessary component of speed, and mark the brick as broken. Well, then we just delete all the broken ones, and if there are none left, we finish the game, we won!

Well, now you can start the game and see how the ball jumps, breaks bricks and ... Yes, but how to beat it? There is a bat, but do we have to control it? I chose the easiest way to control the bat: I allocated a place under it for the "ear", for which you can drag it with your thumb. In order to change the position of the bits, we need to receive events of pressing and moving the finger, and for this we need gesture recognizers! Install them:

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

Well, are you ready to play? Launched, broke all the bricks! So, what is next? It would be necessary to show the player the result of his labors, and therefore display the menu. Usually, separate scenes are made for such things, implemented using the GameScene class inherited from GameSprite, but I decided not to bother and leave the code as simple as possible. So the menu is assembled from different sprites. Well, in glkView:drawInRect:us we need to check the state of the game and draw everything as it should:

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

Here, again, everything is extremely simple, I don’t even want to comment. It remains to process the click on the "play" button, for this, handleTapGestureFrom:insert the "else" block:

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

All! Run, play, win and lose! And most importantly - rejoice! We rejoice at our native, self-written iPhone game on pure OpenGL!


Fuh, everything seems to be. Now I can take a break and give a link to what happened to me, as usual, the sources on the github . (An attentive reader has already noticed that the ball rotates in the video? Well, there is a code on the github for this, but I did not want to overload the article)

Of course, this game is far from perfect. It lacks the animation of breaking a brick, falling bonuses, scoring, changing levels, better definition of collisions, attacks of sounds. Well, you can also add a high score table, integration with Game Center, saving results to iCloud, sending to Facebook / Twitter, achievements and other things without which a modern game is not taken seriously. But in my plans there was no release of the game in the App Store, so I didn’t bother with all this, since the test task didn’t provide for this either.

PS or What's Next:And then there was nothing interesting. I went for an interview at ZeptoLab, but very quickly fell into a lack of knowledge of OpenGL at a deep level. But I got my profit: now I know that I can make a simple game if necessary, and in general I figured out a little in a new area for myself. I hope that now you can write a game, too, my dear reader! For sim, let me take my leave, goodbye.

Also popular now: