The Terrain class

The Terrain class is where we will do the most of our heavy lifting. This is the class that will create the hills in the game. When I say create, I mean give them that beautiful, colorful, materialistic appearance that you saw in the screenshot. In addition to that, this class will also generate the smooth curve of the hill and also create its physics body. Finally, it will be responsible for scrolling and scaling itself with respect to the penguin's position.

The Terrain class will inherit from CCNode. We will define this class in parts, starting with the most exciting part of all, the texture generation. We will then write code to generate the hills and the Box2D body. So, sharpen your pencils and smoothen your paintbrushes. Programmatic texture generation, here we come!

We will implement our Terrain class in the following steps:

  • Generate a striped texture
    • Render the colored stripes
    • Render a gradient over the stripes
    • Render a highlight over the gradient
    • Render a thin border over the highlight
    • Finally apply a noise texture
  • Generate the vertices for the hill
    • Generate key points for the hill
    • Generate vertices to represent the curved surface of the hill

Generating the striped texture

In Chapter 4, Back to the Drawing Board, we did some primitive drawing with Inverse Universe using the CCDrawNode class. However, that was much different in comparison to what we will be doing here. The highlight of this chapter will be using a few OpenGL commands to render some things on our own. It is imperative to have some basic knowledge of OpenGL to make the most of this chapter, so I strongly advise you to consult Google to cover the basics before diving into the code for this chapter. Alternatively, you could just use the OpenGL website as a starting point:

https://www.opengl.org/sdk/docs/man4/

If you were as curious as I was, you must have Ctrl and click or used F12 on the CCDrawNode class to view what it actually does to draw primitives. If you didn't, it actually calculates the vertices for each shape and draws them each frame using OpenGL commands. That is exactly what we will do, but we will save all of that drawing into a texture. Confused? Well, everything will seem vibrant and colorful on the other side of the GenerateStripedSprite function (it's difficult to type and pronounce) from the Terrain.cpp file:

CCSprite* Terrain::GenerateStripedSprite(EStripeType stripe_type, 
  int num_stripes)
{
  // create a texture that we can draw into
  CCRenderTexture* render_texture = CCRenderTexture::create(
    STRIPE_TEXTURE_SIZE, STRIPE_TEXTURE_SIZE);
  // begin with pure black
  render_texture->beginWithClear(0.0f, 0.0f, 0.0f, 0.0f);
  
  RenderStripes(stripe_type, num_stripes);
  RenderGradient();
  RenderHighlight();
  RenderTopBorder();
  RenderNoise();
  
  render_texture->end();
  // create a sprite out of the rendered texture & return it
  return CCSprite::createWithTexture(
    render_texture->getSprite()->getTexture());
}

Before we define this function with the tongue-twisting name, I want to show you the CCSprite that it returns to us:

Generating the striped texture

The preceding image has a set of colored stripes, plus a gradient causing the image to be light at the top and dark at the bottom. That is then followed by a sort of yellow shine on the top of the image plus a very fine border across the top edge. Finally, a noise texture is applied to the image so it has a material appearance instead of looking completely flat.

Now, the first thing we do is create a new CCRenderTexture object, passing in the desired width and height of the resultant texture. We follow that up by calling the beginWithClear function on our new render_texture object. This sets up the renderer so that all subsequent OpenGL commands will draw into the texture instead of the frame buffer and consequently the screen. In addition to that, it also clears the texture and fills it with a color, as indicated by the parameters passed in.

After calling beginWithClear, every subsequent OpenGL command will affect the texture, and thus, we must call the end function of the CCRenderTexture class. This will stop the CCRenderTexture class from grabbing any further OpenGL commands. In essence, all that we want to draw into our custom texture will have to be drawn between these two functions: begin/beginWithClear and end. We will discuss each of the five functions you see in the following sections and I will show you the output after each step.

Drawing the stripes

In our texture, first we draw the stripes that will give our game its signature look. To do that, we define a function named RenderStripes that accepts the type of stripe and the number of stripes. The type of stripe is described by an enum named EStripeType, which you will find in the GameGlobals.cpp file. This is quite a large function with a lot of new things to learn. So, we'll discuss it part by part instead of all at once:

void Terrain::RenderStripes(EStripeType stripe_type, int num_stripes)
{
  // allocate memory for the position & colour arrays
  ccVertex2F* vertices = (ccVertex2F*)malloc(sizeof(ccVertex2F) * num_stripes * 6);
  ccColor4F* colors = (ccColor4F*)malloc(sizeof(ccColor4F) * num_stripes * 6);

  // initialise variables
  int num_vertices = 0;
  float x1 = 0.0f, x2 = 0.0f, y1 = 0.0f, y2 = 0.0f, dx = 0.0f, dy = 0.0f;

  // select between two colours or many colours
  bool two_colors = (CCRANDOM_MINUS1_1() > 0);
  ccColor4F color1 = GameGlobals::GetRandomColor();
  ccColor4F color2 = GameGlobals::GetRandomColor();
  ccColor4F c;

We begin by allocating memory for our two arrays, vertices and colors, which are of the type ccVertex2F and ccColor4F, respectively. These arrays will hold the position and color data at each vertex for each stripe. We then have a counter for the total number of vertices generated followed by variables to represent the coordinates of each vertex composing the stripe. The EStripeType enum that is passed as an argument to this function describes three types of stripes, that is, horizontal, diagonal (top-left to bottom-right), and diagonal (bottom-left to top-right). We will go over the code for each of them separately, starting with the horizontal stripes:

if(stripe_type == E_STRIPE_HORIZONTAL)
  {
    // initialise variables for the horizontal stripe
    dx = 0;
    dy = (float)STRIPE_TEXTURE_SIZE / (float)num_stripes;
    
    x1 = 0;
    y1 = 0;
    
    x2 = STRIPE_TEXTURE_SIZE;
    y2 = 0;
    
    // generate position & colour for each vertex of the stripe
    for (int i = 0; i < num_stripes; ++ i)
    {
      c = two_colors ? (i%2 ? color1 : color2) : 
        GameGlobals::GetRandomColor();

      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertex2(x1, y1);
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertex2(x2, y2);
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertex2(x1, y1 + dy);
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertices[num_vertices - 2];
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertices[num_vertices - 2];
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertex2(x2, y2 + dy);

      y1 += dy;
      y2 += dy;
    }
  }

Before we discuss the code, here is a visual representation of what this code block results in:

Drawing the stripes

As you can see in the preceding figure, each stripe is nothing but a rectangle described by four vertices (v0, v1, v2, v3). However, we will draw this rectangle as two separate triangles. The two triangles will be described by vertices (v0, v1, v2) and (v1, v2, v3) respectively. That makes a total of six vertices for each stripe and that is why we multiply num_stripes by the number six while allocating memory to vertices and colors.

At the top of the block, we have defined the delta movement for both x and y coordinates into variables dx and dy respectively, followed by the initial values for the first stripe. We then write a loop that calculates six vertices for each stripe. In the loop, we either select from the two colors (as in the preceding figure) or we select a random color for each stripe. What follows is code to describe the six vertices (v0, v1, v2) and (v1, v2, v3) that will complete a single stripe. The loop terminates by incrementing y1 and y2 by dy to calculate vertices for the next stripe.

Now, let's look at code that generates vertices for the diagonal stripes:

else
{
  // initialise variables based on type of stripe
  dx = (float)STRIPE_TEXTURE_SIZE * 2 / (float)num_stripes;
  dy = 0;
    
  x1 = -STRIPE_TEXTURE_SIZE;
  y1 = (stripe_type == E_STRIPE_SLOPE_DOWN) ? 0 : 
      STRIPE_TEXTURE_SIZE;
    
  x2 = 0;
  y2 = (stripe_type == E_STRIPE_SLOPE_DOWN) ? 
    STRIPE_TEXTURE_SIZE : 0;
    
  // generate position & colours for two stripes at a time
  for (int i = 0; i < num_stripes / 2; ++ i)
  {
    c = two_colors ? (i%2 ? color1 : color2) : 
      GameGlobals::GetRandomColor();

    for(int j = 0; j < 2; ++ j)
    {
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertex2(x1 + j * 
        STRIPE_TEXTURE_SIZE, y1);
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertex2(x1 + j * 
        STRIPE_TEXTURE_SIZE + dx, y1);
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertex2(x2 + j * 
        STRIPE_TEXTURE_SIZE, y2);
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertices[num_vertices - 2];
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertices[num_vertices - 2];
      colors[num_vertices] = c;
      vertices[num_vertices ++] = vertex2(x2 + j * 
        STRIPE_TEXTURE_SIZE + dx, y2);
    }

    x1 += dx;
    x2 += dx;
  }
}

Before we discuss the code, here is a visual representation of what this code block results in when the stripe type is E_STRIPE_SLOPE_DOWN:

Drawing the stripes

The logic we'll use to draw diagonal stripes will be quite different as compared to horizontal stripes. As you can see in the preceding figure, vertex v0 of the first stripe starts far out to the left of the actual texture (indicated by the black outlined square in the center). Thus, we initialize x1 with a value equal to -STRIPE_TEXTURE_SIZE and x2 with 0.

The variables y1 and y2 are defined based on whether our stripe will be top-left to bottom-right (E_STRIPE_SLOPE_DOWN) or bottom-left to top-right (E_STRIPE_SLOPE_UP). We then define a nested structure of loops that draws two stripes at a time. So, the vertices generated within the inner for loop will be for stripe-1 (green) and stripe-3 (green), then stripe-2 (yellow) and stripe-4 (yellow), and so on.

Now that we've calculated vertices for the various types of stripes, it is time to actually make the OpenGL calls that will do the rendering for us. So, let's take a look at the last part of the RenderStripes function:

// we're dealing with position & colour data here
setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionColor));
CC_NODE_DRAW_SETUP();

// enable position & colour attributes
ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position | kCCVertexAttribFlag_Color);
// pass position & colour data
glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, vertices);
glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_FLOAT, GL_TRUE, 0, colors);
// set the blend function
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
// draw it...GL_TRIANGLES style!
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)num_vertices);

// free what we allocated on top
free(vertices);
free(colors);

We kick off the rendering by setting the shader program for this node. Shaders are a huge aspect of graphics programming, but unfortunately they're way out of the scope of this book. For now, all you need to know is that shaders are simply programs that run on the GPU. Cocos2d-x comes with a bunch of default shaders that are loaded into the CCShaderCache class. You can also create and cache your own shaders into this class.

Here, we're rendering colored stripes and we're dealing only with the position and color for each stripe, which is why we select the kCCShader_PositionColor type of shader. You can find all the default shaders at the following path:

cocos2d-x-2.2.5cocos2dxshaders

We then use a convenience macro CC_NODE_DRAW_SETUP() that sets up the GL server state and links the shader program we set. We then call the ccGLEnableVertexAttribs function, passing in the flags kCCVertexAttribFlag_Position and kCCVertexAttribFlag_Color. This function informs OpenGL which attributes of the vertex will be used while rendering. The ccGLEnableVertexAttribs function has OpenGL function glEnableVertexAttribArray at its heart.

Note

You can find more information on glEnableVertexAttribArray at https://www.khronos.org/opengles/sdk/docs/man/xhtml/glEnableVertexAttribArray.xml.

Next, we make two calls to the glVertexAttribPointer function, one each for the position and color attributes that we just enabled. This function basically tells OpenGL how to interpret the data that we're passing into this function via the arrays vertices and colors.

Note

You can find more information on glVertexAttribPointer at https://www.khronos.org/opengles/sdk/docs/man/xhtml/glVertexAttribPointer.xml.

We must now set the blend mode by calling glBlendFunc, which basically specifies how the incoming values (the colored stripes we're about to render) will affect the existing values (so far, we have nothing, though). We pass in GL_ONE as the source factor, which means that all the existing values will be carried forward, and we pass in GL_ONE_MINUS_SRC_ALPHA as the destination factor, which means that the incoming values will be rendered wherever there is transparency in the existing values.

Note

You can use this incredible visual tool to help you understand blend functions (http://www.andersriggelsen.dk/glblendfunc.php).

Finally, we call the glDrawArrays function that renders the vertex data that we just passed through glVertexAttribPointer.

The first parameter to glDrawArrays, GL_TRIANGLES, tells OpenGL that it should use all three vertices to draw a single triangle. The second parameter is the starting index from within the array where the vertices will be read. The last parameter is simply the number of vertices that we want rendered.

Note

You can find more information on the glDrawArrays function at https://www.khronos.org/opengles/sdk/docs/man/xhtml/glDrawArrays.xml.

Now that we're done with the rendering, we must free the two arrays that we allocated memory to, at the start of the function. This winds up the RenderStripe function.

Adding a gradient

Now that we've drawn our stripes, we can go ahead and beautify the texture with a gradient. Remember that this texture will be applied to a landscape of mountains and we need to create an illusion of a shadow. Thus, our gradient will be light at the top and get darker towards the bottom. Let's take a look at the RenderGradient function from Terrain.cpp:

void Terrain::RenderGradient()
{
  // declare arrays for position & colour data
  ccVertex2F vertices[4];
  ccColor4F colors[4];

  // gradient will be light on top & dark at the bottom
  vertices[0] = vertex2(0, 0);
  vertices[1] = vertex2(STRIPE_TEXTURE_SIZE, 0);
  vertices[2] = vertex2(0, STRIPE_TEXTURE_SIZE);
  vertices[3] = vertex2(STRIPE_TEXTURE_SIZE, STRIPE_TEXTURE_SIZE);
  colors[0] = ccc4f(0.0f, 0.0f, 0.0f, 0.0f);
  colors[1] = ccc4f(0.0f, 0.0f, 0.0f, 0.0f);
  colors[2] = ccc4f(0.0f, 0.0f, 0.0f, 0.75f);
  colors[3] = ccc4f(0.0f, 0.0f, 0.0f, 0.75f);

  // we're dealing with position & colour data here
  setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(
    kCCShader_PositionColor));
  CC_NODE_DRAW_SETUP();

  // enable position & colour attributes
  ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position | 
    kCCVertexAttribFlag_Color);
  // pass position & colour data
  glVertexAttribPointer(kCCVertexAttrib_Position, 2, 
    GL_FLOAT, GL_FALSE, 0, vertices);
  glVertexAttribPointer(kCCVertexAttrib_Color, 4, 
    GL_FLOAT, GL_FALSE, 0, colors);
  // draw it...GL_TRIANGLE_STRIP style!
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

At the start of the function, we define two arrays to hold the position and color information for the gradient. We then fill in the positions as the top-left, top-right, bottom-left, and bottom-right corners into the vertices array. This is followed by filling in the color data, which is a completely transparent black color at the top with 75 percent opacity at the bottom.

Just by doing that, we have created a gradient, as OpenGL will automatically interpolate the alpha values between 0 and 75 percent when it runs the position-color shader. I am sure that you noticed how the arrays have only four elements instead of the six we had for the stripes. This is because we use a different drawing mode, named GL_TRIANGLE_STRIP, this time. GL_TRIANGLE_STRIP informs OpenGL that every triangle it draws will have the first two vertices same as the last two vertices of the previous triangle.

Thus, at the time of rendering, the gradient will be composed of two triangles having vertices {(0, 0), (STRIPE_TEXTURE_SIZE, 0), (0, STRIPE_TEXTURE_SIZE)} and {(STRIPE_TEXTURE_SIZE, 0), (0, STRIPE_TEXTURE_SIZE), (STRIPE_TEXTURE_SIZE, STRIPE_TEXTURE_SIZE)}, respectively.

I'm sure you're wondering why we didn't use GL_TRIANGLE_STRIP while rendering the stripes given that we could have saved two vertices for each stripe. Since GL_TRIANGLE_STRIP causes triangles to share their vertices, all the triangles it draws must be connected or adjacent to each other. If you remember, while calculating vertices for the diagonal stripes, we were drawing two stripes that were apart from each other. That's the reason why we had to use GL_TRIANGLES. So far, this is what our texture looks like:

Adding a gradient

Adding some highlight

We will add some more realism to our texture by giving it some shine at the top to resemble sunlight falling on the surface of the hill. The RenderHighlight function from the Terrain.cpp file will do exactly that:

void Terrain::RenderHighlight()
{
  // declare arrays for position & colour data
  ccVertex2F vertices[4];
  ccColor4F colors[4];

  // highlight will be yellowish on top & nothing at the bottom
  vertices[0] = vertex2(0, 0);
  vertices[1] = vertex2(STRIPE_TEXTURE_SIZE, 0);
  vertices[2] = vertex2(0, STRIPE_TEXTURE_SIZE/3);
  vertices[3] = vertex2(STRIPE_TEXTURE_SIZE, STRIPE_TEXTURE_SIZE/3);
  colors[0] = ccc4f(1.0f, 1.0f, 0.5f, 0.4f);
  colors[1] = ccc4f(1.0f, 1.0f, 0.5f, 0.4f);
  colors[2] = ccc4f(1.0f, 1.0f, 0.5f, 0.0f);
  colors[3] = ccc4f(1.0f, 1.0f, 0.5f, 0.0f);

  // we're dealing with position & colour data here
  setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(
    kCCShader_PositionColor));
  CC_NODE_DRAW_SETUP();

  // enable position & colour attributes
  ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position | 
    kCCVertexAttribFlag_Color);
  // pass position & colour data
  glVertexAttribPointer(kCCVertexAttrib_Position, 2, 
    GL_FLOAT, GL_FALSE, 0, vertices);
  glVertexAttribPointer(kCCVertexAttrib_Color, 4, 
    GL_FLOAT, GL_FALSE, 0, colors);
  // set the blend function
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  // draw it...GL_TRIANGLE_STRIP style!
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

Much of the code here is similar to the function RenderGradient, except that the highlight will be applied only to the upper one-third of the texture and the color will be a shade of yellow. Another difference here is the presence of the function glBlendFunc. This is what our texture looks like with some highlight applied:

Adding some highlight

Drawing the border

We now add a thin border to the top of the texture, which when applied to the hills will provide an outline and represent the surface of the hill. The RenderTopBorder function from Terrain.cpp will take care of that. This function bears resemblance to the last two functions, so we shall skip discussing it in detail. The function simply renders a 3-pixel thick gray rectangle at the top of the texture. I'm sure you'll understand this function when you look at it in the source bundle.

Moving on, this is how the texture will look after this function has returned. I've taken the liberty of zooming in so that you can see the border clearly:

Drawing the border

Adding some noise

We've added a lot of effects to the texture, but it still lacks a material feel and it appears flat. So, we'll add some noise to the texture in the RenderNoise function from the Terrain.cpp file:

void Terrain::RenderNoise()
{
  // create a sprite with readymade noise
  CCSprite* noise = CCSprite::create("noise1.png");
  // set the proper blend function
  ccBlendFunc blend_func;
  blend_func.src = GL_DST_COLOR;
  blend_func.dst = GL_ZERO;
  noise->setBlendFunc(blend_func);
  // position the sprite at the centre of the texture
  noise->setPosition(ccp(STRIPE_TEXTURE_SIZE/2, 
    STRIPE_TEXTURE_SIZE/2));
  // call visit to render the sprite...twice gives added contrast
  noise->visit();
  noise->visit();
}

I'm sure you were expecting more than that! Well, every CCSprite class defines the similar set of OpenGL commands in its draw function like the ones we just wrote. So, all we really need to do here is create a CCSprite with a pre-rendered noise texture and call its visit function.

With the RenderNoise function, we complete the generation of a multicolor, realistic, striped texture. With the complicated rendering behind our back, we can now move forward to the creation of the hills. Before that, this is what the texture looks like after the RenderNoise function has returned:

Adding some noise

Generating the hills

The shape of the hills in Penguins Can Fly won't be ultra-realistic, but they should be diverse enough to offer the player an engaging and exciting experience. We will split the algorithm to generate the structure of the hills into two parts: generation of key points and generation of smooth curves. The key points will basically be the peak and bottom points of the hill. After these points are generated, we will have a staircase structure with flat lines between the peak and bottom. We will then write code to interpolate a smooth curve between these key points.

Generating the hill key points

The hill key points will be nothing but the highest and lowest points on the surface of the hill. We must ensure that a peak is followed by a bottom, and vice versa, to ensure smooth continuity. Also, we would like to control the amount by which a peak rises and the amount a bottom falls. This would enable us to make the terrain easier or more difficult. Let's now take a look at the GenerateHillKeyPoints function from the Terrain.cpp file:

void Terrain::GenerateHillKeyPoints(float start_x)
{
  // initialise variables
  num_hill_key_points_ = 0;

  float x, y, dx, dy, ny;

  // first hill key point will be a bit outside the left edge of the screen
  x = start_x - SCREEN_SIZE.width * 0.25f;
  y = 0;
  hill_key_points_[num_hill_key_points_ ++] = vertex2(x, y);

  // the first peak
  x = start_x;
  y = SCREEN_SIZE.height * 0.4f;
  hill_key_points_[num_hill_key_points_ ++] = vertex2(x, y);

We begin this function by initializing the number of hill key points and variables that will represent the x and y coordinates of each key point. We now initialize the first hill key point as being outside the left edge of the screen and right at the bottom. We then create the first peak at the x coordinate that is passed to this function and at a considerable height from the bottom of the screen. Continuing with the GenerateHillKeyPoints function from the Terrain.cpp file:

  // set the minimum & range quantities
  int min_dx = 160, range_dx = 80;
  int min_dy = 60,  range_dy = 60;
  // +1 - going up, -1 - going  down
  float sign = -1; 
  // set the limits
  float max_height = SCREEN_SIZE.height * 0.5f;
  float min_height = SCREEN_SIZE.height * 0.25f;

Now that we've specified how the start of the hill will look, we can write a simple algorithm to generate the rest of the hill. First, we need to define a few parameters for the algorithm. The min_dx and min_dy variables will represent the minimum distance any two hill key points will have between each other, whereas the range_dx and range_dy variables will be used to select a random distance value that will be added to min_dx and min_dy. The sign variable will be toggled between 1 and -1 so that the algorithm generates a peak followed by a bottom followed by a peak and so on. Finally, we define a couple of constraints that prevent the algorithm from generating key points that are too high or too low.

In the GameGlobals.h file, you will find MAX_HILL_KEY_POINTS defined as 100. So, we have 50 peaks and 50 bottoms.

Let's look at the following code:

// first set of points
while (num_hill_key_points_ < MAX_HILL_KEY_POINTS - 15)
{
  dx = CCRANDOM_0_1() * range_dx + min_dx;
  x += dx;
  dy = CCRANDOM_0_1() * range_dy + min_dy;
  ny = y + dy * sign;
  if(ny > max_height) ny = max_height;
  if(ny < min_height) ny = min_height;
  y = ny;
  sign *= -1;
  hill_key_points_[num_hill_key_points_++] = vertex2(x, y);
}

In the preceding code, we define the majority of the hill key points. The reason we don't define them all in a single loop is because we want to create a kind of ramp at the end of the hill for the penguin to launch off. As you read earlier, we use range_dx/dy in combination with min_dx/dy to generate a delta into dx and dy. We then multiply the y coordinate with sign and restrict it within the appropriate constraints. Finally, we toggle sign by negating it and save the new hill point as a ccVertex2F object in the hill_key_points_ array.

Let's now look at the last part of the function that takes care of generating the ramp-like structure towards the end of the hill. This loop will gradually lower the peak and bottom hill key points so that the last peak is high enough to act like a ramp for the penguin to take off.

Let's look at the following code:

// points that will go lower and lower
  min_height = SCREEN_SIZE.height * 0.1f;
  while (num_hill_key_points_ < MAX_HILL_KEY_POINTS - 2)
  {
    dx = CCRANDOM_0_1() * range_dx + min_dx;
    x += dx;
    dy = CCRANDOM_0_1() * range_dy + min_dy;
    ny = ( (y + dy * sign) < hill_key_points_[
      num_hill_key_points_ - 2].y ) ? (y + dy * sign) : 
      (y + dy * sign * 0.5f);
    if(ny < min_height) ny = min_height;
    y = ny;
    sign *= -1;
    hill_key_points_[num_hill_key_points_++] = vertex2(x, y);
  }

  // finally a nice upward slope...the ramp to launch the penguin
  x += min_dx + range_dx * 3;
  y += min_dy + range_dy * 1.5f;
  hill_key_points_[num_hill_key_points_++] = vertex2(x, y);

  // last point will be way down below
  x += min_dx + range_dx * 1.5f;
  y = 0;
  hill_key_points_[num_hill_key_points_++] = vertex2(x, y);

  // initialise left most & right most key points
  from_key_point_ = 0;
  to_key_point_ = 0;
}

We start the loop by lowering min_height to SCREEN_SIZE.height * 0.1f from SCREEN_SIZE.height * 0.25f. This while loop is similar to the previous one except that it possesses a condition that ensures each peak is lower in elevation than the previous peak. The loop still doesn't create the ramp for us though. That is why we add the last two hill key points by ourselves.

Notice how the first hill key point that we add manually has the range multiplied so as to increase the distance between this key point and the previous, thereby creating a ramp. We then finish the hill structure off by creating the last key point at the bottom of the screen. We will discuss the from_key_point_ and to_key_point_ variables in a bit. For now, I have a screenshot of the hill key points that are generated by this function, so you know exactly what we have achieved:

Generating the hill key points

Generating the curve of the hill

In the preceding screenshot, you can tell that we have the key points correct. But this is not gameplay material. In order to make things realistic and fun, we will need to give the hills a smooth, curved surface over which the penguin can easily slide and take off too. We will make use of a simple cosine curve to achieve a neatly interpolated curve between two hill key points. If you're rusty on your trigonometry, this is what a cosine curve looks like:

Generating the curve of the hill

So, we want the y coordinates of the slope to be a factor of a cosine curve as it passes from 0 to pi. All this happens in the GenerateBorderVertices function of Terrain.cpp:

void Terrain::GenerateBorderVertices()
{
  // initialise variables
  num_border_vertices_ = 0;

  ccVertex2F p0, p1, pt0, pt1;
  p0 = hill_key_points_[0];

  for (int i = 1; i < num_hill_key_points_; ++ i)
  {
    p1 = hill_key_points_[i];

    // calculate the number of segments between adjacent key points
    int h_segments = floorf((p1.x - p0.x) / HILL_SEGMENT_WIDTH);
    // calculate delta x
    float dx = (p1.x - p0.x) / h_segments;
    // calculate delta theta
    float da = M_PI / h_segments;
    // calculate x-axis & amplitude for the cosine wave
    float ymid = (p0.y + p1.y) / 2;
    float ampl = (p0.y - p1.y) / 2;
    pt0 = p0;
    border_vertices_[num_border_vertices_++] = pt0;

    // for each segment, calculate x & y coordinate
    for (int j = 1; j < h_segments + 1; ++ j)
    {
      // x coordinate is last coordinate plus delta
      pt1.x = p0.x + j * dx;
      // y coordinate taken from the cosine wave
      pt1.y = ymid + ampl * cosf(da * j);
      border_vertices_[num_border_vertices_ ++] = pt1;
      pt0 = pt1;
    }

    p0 = p1;
  }
}

At the start of the function, we initialize the various variables and counters.

Note

Remember that we will interpolate between two hill key points. These two key points will be represented by the variables p0 and p1 within the loop.

We initialize p0 to point to the first key point in the hill_key_points_ array and start the for loop from 1 instead of 0.

Inside the loop, we initialize p1 and calculate the number of segments between p0 and p1 based on the width of each segment (HILL_SEGMENT_WIDTH value as 15). We must calculate the delta for the x coordinate and for the angle (theta). We then calculate the midpoint of the segment between p0 and p1 in terms of the y coordinate into the ymid variable. This will act as the x axis for our cosine curve. We also define an amplitude that we will multiply by the cosine curve; the larger the amplitude, the greater the distance between the peak and bottom of the cosine curve.

We then run a loop to interpolate from p0 to p1, one segment at a time. Inside the loop, we calculate the vertices of the individual segment that will make up the curve. Notice how we use the cosf function to calculate the y coordinate of the curve. The following screenshot shows what happens when the points generated in the border_vertices_ array are joined:

Generating the curve of the hill

In this screenshot, you can see the hill key points and the smooth curve that is generated around them. At this point, we have a smoothly interpolated surface of the hill. We can now build a Box2D body with the vertices generated in the GenerateBorderVertices function.

Creating the Box2D body for the hill

The Box2D body of the hill will be static and its fixture will contain a chain shape. Let's take a look at what exactly happens in the CreateBody function of Terrain.cpp:

void Terrain::CreateBody()
{
  // create a body only the first time...after that only 
    create fixture
  if(body_ == NULL)
  {
    b2BodyDef bd;
    bd.position.Set(0, 0);
    body_ = world_->CreateBody(&bd);
  }
  
  // create array for the vertices
  b2Vec2 vertices[MAX_BORDER_VERTICES];
  int num_vertices = 0;
  // loop through border_vertices_, convert screen 
    coordinates to physics coordinates
  for (int i = 0; i < num_border_vertices_; ++ i)
  {
    vertices[num_vertices ++].Set(SCREEN_TO_WORLD(
      border_vertices_[i].x), SCREEN_TO_WORLD(border_vertices_[i].y));
  }

  // finish up the last two vertices to form a loop
  vertices[num_vertices ++].Set(SCREEN_TO_WORLD(border_vertices_[
    num_border_vertices_ - 1].x), 0);
  vertices[num_vertices ++].Set(SCREEN_TO_WORLD(
    border_vertices_[0].x), 0);
  
  // create the chain fixture with above vertices
  b2ChainShape shape;
  shape.CreateChain(vertices, num_vertices);
  body_->CreateFixture(&shape, 0);
}

We begin by creating a static b2Body object into the member variable body_ of class Terrain. We then declare an array of type b2Vec2 named vertices before filling it up with the vertices we just calculated and fed into border_vertices_. Notice the helper macro SCREEN_TO_WORLD converting the coordinates from pixels to meters. Once this is done, we simply create a b2ChainShape object passing in the vertices array along with the number of vertices the chain should have. We wind up this function by creating a new fixture that will glue body_ to the chain shape.

In our Terrain class, we have written code to generate a striped texture, generate vertices for a smooth curved hill, and even created a Box2D body. We still haven't rendered the hill though. We still need to bind the striped texture to the vertices of the curve and render the hill.

Rendering the hills

Before we write code to render the hills inside the draw function, we need to save the texture we generated by calling the GenerateStripedSprite function. So, let's take a look at a part of the init function from Terrain.cpp:

bool Terrain::init(b2World* world, float start_x)
{
  .
  .
  .
  // select between a type of stripe
  EStripeType stripe_type = (EStripeType)((int)(CCRANDOM_0_1() * 
    (E_STRIPE_SLOPE_DOWN + 1)));
  // generate the stiped sprite
  sprite_ = GenerateStripedSprite(stripe_type , 8);
  // retain for use since we won't be adding it
  sprite_->retain();
  // setup the texture to repeat and stick to the edge
  ccTexParams tex_params;
  tex_params.minFilter = GL_LINEAR;
    tex_params.magFilter = GL_LINEAR;
  tex_params.wrapS = GL_REPEAT;
    tex_params.wrapT = GL_CLAMP_TO_EDGE;
  sprite_->getTexture()->setTexParameters(&tex_params);
  .
  .
  .
  .
}

Before we generate a texture, we must choose what type of stripes we want it to have. We then pass this stripe type into the GenerateStripedSprite function. If you remember, the GenerateStripedSprite function returns a pointer to a new CCSprite, which we now save into a member variable of class Terrain named sprite_. Since we won't really be adding the sprite_ to the scene graph, we must retain it.

We also define the texture parameters for this sprite's texture, since we intend on repeating it along the vertices of the hill. Doing this will keep the sprite ready to be bound to the hill's vertices, but it won't actually render anything. So, we must define the draw function that class Terrain will override from CCNode as follows:

void Terrain::draw()
{
  // can't render without a sprite
  if(sprite_ == NULL)
  {
    return;
  }

  CC_NODE_DRAW_SETUP();

  // bind the texture for this node
  ccGLBindTexture2D(sprite_->getTexture()->getName());
  // enable position & colour attributes
  ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position | kCCVertexAttribFlag_TexCoords);
  // pass position & colour data
  glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, hill_vertices_);
  glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, hill_tex_coords_);
  // draw it...GL_TRIANGLE_STRIP style!
  glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)num_hill_vertices_);
}

We begin by checking whether the stripe texture sprite exists. You should be familiar with the code in this function by now, so I'll only highlight the differences. We call the ccGLBindTexture2D function here and pass in the name of the striped texture stored within sprite_. This is a method from Cocos2d-x that essentially calls the OpenGL command glBindTexture. The glBindTexture function simply binds a texture pointed to by the specified name to the active texture unit, which in this case is the node we're rendering.

Note

You can find more information on glBindTexture at https://www.khronos.org/opengles/sdk/docs/man/xhtml/glBindTexture.xml.

Another difference I'm sure you noticed is the flags we passed into ccGLEnableVertexAttribs. In this case, we're passing in the position and texture coordinates and not colors, so we enable the flags kCCVertexAttribFlag_Position and kCCVertexAttribFlag_TexCoords respectively. We then pass the position data stored inside hill_vertices_ and the texture coordinate data stored inside hill_tex_coords_ to OpenGL before calling glDrawArrays with the GL_TRIANGLE_STRIP draw mode.

But when did we create the arrays hill_vertices_ and hill_tex_coords_? Well, we haven't yet. So, let's look at the ResetVertices function from Terrain.cpp and see how these arrays are filled up. This one is a slightly large function, so we will look at it in parts:

void Terrain::ResetVertices()
{
  // calculate the area of the hill that is currently visible plus a buffer of 
    0.125 * screen width
  float left_side = offset_x_ - SCREEN_SIZE.width * 
    0.125f / m_fScaleX;
  float right_side = offset_x_ + SCREEN_SIZE.width * 
    1.125f / m_fScaleX;
  
  // loop to calculate the left most key point
  while (hill_key_points_[from_key_point_ + 1].x < left_side)
  {
    from_key_point_ ++;
    if (from_key_point_ > num_hill_key_points_ - 1) {
      from_key_point_ = num_hill_key_points_ - 1;
      break;
    }
  }

  // loop to calculate the right most key point
  while (hill_key_points_[to_key_point_].x < right_side)
  {
    to_key_point_ ++;
    if (to_key_point_ > num_hill_key_points_ - 1) {
      to_key_point_ = num_hill_key_points_ - 1;
      break;
    }
  }

We begin this function by calculating the left and right limits of the hill. These limits are important because we will render only that area of the hill that is contained within these limits, as opposed to the entire hill. The offset_x_ variable that is used in this calculation is nothing but the horizontal distance the hill has moved from its initial position. We must update offset_x_ with respect to the position of the penguin to ensure that the hills scroll but more on that later.

Next, we have two while loops to identify the left-most and right-most hill key points based on left_side and right_side. We store these values into from_key_point_ and to_key_point_ respectively. If you remember, both these variables were initialized to 0 at the end of the GenerateHillKeyPoints function. We can use these two variables to govern how many hill key points will be used in rendering the hill, which is handled in the next part of the ResetVertices function:

// only loop if visible key points have changed
  if (prev_from_key_point_ != from_key_point_ || 
    prev_to_key_point_ != to_key_point_)
  {
    // initialise variables
    num_hill_vertices_ = 0;
    ccVertex2F p0, p1, pt0, pt1;
    p0 = hill_key_points_[from_key_point_];

    // calculate curve vertices from left most to right most key point
    for(int i = from_key_point_ + 1; i < to_key_point_ + 1; ++ i)
    {
      p1 = hill_key_points_[i];

      // calculate the number of segments between adjacent key points
      int h_segments = floorf((p1.x - p0.x) / HILL_SEGMENT_WIDTH);
      int v_segments = 1;
      // calculate delta x
      float dx = (p1.x - p0.x) / h_segments;
      // calculate delta theta
      float da = M_PI / h_segments;
      // calculate x-axis & amplitude for the cosine wave
      float ymid = (p0.y + p1.y) / 2;
      float ampl = (p0.y - p1.y) / 2;
      pt0 = p0;

      // calculate vertices for each segment
      for(int j = 1; j < h_segments + 1; ++ j)
      {
        pt1.x = p0.x + j * dx;
        pt1.y = ymid + ampl * cosf(da * j);

        // calculate vertices for two triangles...cuz we render 
          using GL_TRIANGLE_STRIP
        for(int k = 0; k < v_segments + 1; ++ k)
        {
          hill_vertices_[num_hill_vertices_] = vertex2(pt0.x, 
            pt0.y - (float)STRIPE_TEXTURE_SIZE / v_segments * k);
          hill_tex_coords_[num_hill_vertices_++] = vertex2(
            pt0.x /(float)STRIPE_TEXTURE_SIZE, (float)k / v_segments);
          hill_vertices_[num_hill_vertices_] = vertex2(pt1.x, 
            pt1.y - (float)STRIPE_TEXTURE_SIZE / v_segments * k);
          hill_tex_coords_[num_hill_vertices_++] = vertex2(
            pt1.x / (float)STRIPE_TEXTURE_SIZE, (float)k / v_segments);
        }
        pt0 = pt1;
      }

      p0 = p1;
    }

    // update previous left most & right most visible key points
    prev_from_key_point_ = from_key_point_;
    prev_to_key_point_ = to_key_point_;
  }
}

We start with an if condition that checks if the new key points calculated are not the same as before. This optimizes our code a bit. Within the conditional, we initialize the number of hill vertices that we will generate in this code block. We also initialize a few variables that are too similar to the ones we saw in the GenerateBorderVertices function. In fact, if you look closely, you will realize that this function is similar to GenerateBorderVertices, which generates the curved surface of the hill.

However, there are a few differences that I will highlight. The first difference is the absence of border_vertices_, since the purpose of this function is to determine the vertices and texture coordinates required for rendering. Hence, we have the hill_vertices_ and hill_tex_coords_ arrays where we save the appropriate data.

There is also an additional variable, v_segments, which in turn spawns a new inner for loop. The inner for loop simply uses the variables pt0 and pt1 to calculate the vertices for two triangles (GL_TRIANGLE_STRIP) to render each horizontal segment. These vertices are stored in the hill_vertices_ array. The following diagram will help you understand what the triangles' vertices will be:

Rendering the hills

In the preceding figure, each horizontal segment (pt0 -> pt1) will be drawn by two triangles (v0, v1, v2) and (v1, v2, v3).

Along with the triangle vertices, the loop also calculates the texture coordinate for each of the vertices. Remember that texture coordinates usually go from 0 to 1. So, for the x component of the texture coordinate, we will divide the x component of the particular vertex by the size of the texture. Since the texture parameters are set to repeat, we don't need to worry about anything else.

For the y coordinate, we will just set the bottom texture coordinate to 0 and the top texture coordinate to 1. This way, the texture is fully distributed vertically. Finally, we update the prev_from_key_point_ and prev_to_key_point_ to from_key_point_ and to_key_point_, respectively. In this way, we have calculated the vertices and texture coordinates that will be used in the draw function.

Updating the hill

The Update function of the Terrain class is called every tick from GameWorld with the penguin's position passed as an argument. The Update function takes care of two important tasks:

  • It updates the position and scale of the hill based on the penguin's position
  • It resets the current hill and generates a new one once the hill has left the screen

Let's see how this is done in the Update function of Terrain.cpp:

void Terrain::Update(CCPoint penguin_position)
{
  // determine current height & minimum height
  float height = penguin_position.y;
  const float min_height = SCREEN_SIZE.height * 0.6f;
  height = (height < min_height) ? min_height : height;
  
  // scale only if penguin is above SCREEN_SIZE.height * 0.6f
  float scale = min_height / height;
  setScale(scale * 1.25f);
  // update scrolling
  SetOffsetX(penguin_position.x);

  // check if terrain has left screen
  if(from_key_point_ >= MAX_HILL_KEY_POINTS - 1 && to_key_point_ >= 
    MAX_HILL_KEY_POINTS - 1)
  {
    // reset the old data
    Reset();
    // create a new hill a couple of screens ahead of the penguin
    init(world_, penguin_position.x + SCREEN_SIZE.width * 2);
  }
}

We begin by storing the elevation of the penguin into the height variable. We also initialize another variable min_height with the value SCREEN_SIZE.height * 0.6f. We then use these variables to calculate the amount that the entire hill should be scaled into the scale variable. The calculation works in such a way that hill only scales down when the penguin's y coordinate has risen above 0.6 times the height of the screen. We also pass the x component of the penguin's position to the SetOffsetX function that we will discuss next.

The last part of the Update function checks to see whether the hill has scrolled out of the screen by comparing the values of the from_key_point_ and to_key_point_ variables. These variables specify the left-most and right-most key points of the visible area of the hill. Thus, if both variables are past the total number of key points, it is safe to say that the hill has left the screen.

In that case, we simply call the Reset function of Terrain, which makes sure that the striped sprite is released and the body's fixtures are destroyed while also resetting all the appropriate counters and variables. After Reset, we call the init function and pass in a reference to the physics world and the starting point for the next set of hills. We position the next set of hills a couple of screens ahead of the penguin, giving the players a second to catch their breath.

Let's now take a look at the SetOffsetX function that is responsible for scrolling the hills:

void Terrain::SetOffsetX(float offset_x)
{
  // update only if offset is different or if its a new game
  if (offset_x_ != offset_x || first_time_)
  {
    first_time_ = false;
    offset_x_ = offset_x;
    // leave some gap so the penguin is not stuck to the left 
      of the screen
    setPositionX(SCREEN_SIZE.width / 8 - offset_x_ * m_fScaleX);

    // reset the drawable vertices of the hill
    ResetVertices();
  }
}

The if condition ensures that we proceed further only when the offset passed into this function is different from the existing offset or if this is the first time SetOffsetX is called. What follows next is code to set the position of the hill based on the offset and scale factor, while also allowing the player to stay 0.125 times the screen width ahead of the terrain. Finally, we call the ResetVertices function that will update the vertices and texture coordinates so the appropriate area of the hill is rendered.

At this point, our Terrain class is capable of generating a realistic yet beautiful striped texture, creating smooth and interpolated vertices for a hill. It also creates a physics body along those vertices, rendering itself and even scrolling and scaling itself. Now, it's time to look at the other main entity of the game: the Penguin class.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.178.181