Create the main application loop

Now that we've completed the required initialization for our application, it's time to start coding the application in earnest. We create a main application loop where we simply call three functions over and over again until the user directs the application to close:

  • The handle_events() function processes any change in accelerometer data, determines whether an event has occurred, determines what the event is, and responds accordingly.
  • The update() function updates the positions of all of the blocks that are currently on the screen.
  • The render() function draws the blocks (in their updated positions) on the screen.

while (!shutdown) {
    // Handle user input and sensors
    handle_events();

    //Update cube positions
    update();

    // Draw Scene
    render();
}

Handle events

We want the Falling Blocks application to be able to process and respond to events, such as when a user touches the screen to add a new block, so we'll take care of this first.

  1. The BlackBerry Platform Services library categorizes events into domains, so, in handle_events(), set up local variables that store the event domains to be used by the application.
  2. In handle_events(), request the next available event from the BlackBerry Platform Services library. We pass 0 to bps_get_event() to indicate that this function should return immediately (even if no new events are available) instead of blocking. This means that we're polling for new events each time the main application loop executes.

    bps_event_t *event = NULL;
    rc = bps_get_event(&event, 0);

  3. Determine what type of event occurred by checking to see which domain the event is part of. During the initialization of Falling Blocks, we requested events from the screen, navigator, and sensors, so we check these domains and invoke the appropriate handler function.

    if (event) {
        int domain = bps_event_get_domain(event);
    
        if (domain == screen_domain) {
            handleScreenEvent(event);
    			 } else if (domain == navigator_domain) {
    				    handleNavigatorEvent(event);
    			 } else if (domain == sensor_domain) {
    				    handleSensorEvent(event);
        }
    }

  4. If the event is a screen event, do the following in handleScreenEvent():
    1. Extract the screen event from the generic BlackBerry Platform Services event that we received by invoking screen_event_get_event(). This function is part of a set of libscreen functions. Libscreen functions allow you to interact directly with the screen on the tablet. We'll use more libscreen functions in the next step to retrieve screen event properties.

      screen_event_t screen_event = screen_event_get_event(event);

    2. Determine the type of screen event that occurred and the position of the event on the screen. Every screen event is associated with a set of properties. You can query these properties by using the screen_get_event_property_iv() function, which is part of screen.h. This library also lists other properties that you can query, so check there to find the ones that you're interested in for your own applications. Here, we determine the type of screen event and store it in screen_val, and we determine the location that the event occurred and store the coordinates in pair.

      screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_TYPE, &screen_val);
      screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_SOURCE_POSITION, pair);

    3. Determine whether screen_val refers to a touch event or a mouse pointer event. The BlackBerry Native SDK for Tablet OS differentiates between a touch screen event (which occurs on a tablet) and a mouse pointer event (which occurs on a BlackBerry Tablet Simulator or when a USB mouse is attached to a tablet), so we must check for each type of event to make sure that our application works properly on a tablet and the simulator. In the case of a mouse pointer event, we want to add a block to the screen only when a user releases the mouse button. We use a debounce technique to accomplish this, meaning that after we detect the first mouse click event, we only add a block the next time we don't detect a mouse click event. In each case, we add a block to the screen, either when a user finishes touching the screen or when the user releases the mouse button.

      if (screen_val == SCREEN_EVENT_MTOUCH_RELEASE) {
          //This is touch screen event.
          add_cube((float)pair[0], (float)pair[1]);
      }
      else if (screen_val == SCREEN_EVENT_POINTER) {
          //This is a mouse move event, it is applicable to a device with a
          //USB mouse or simulator
          screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_BUTTONS,
                                       &buttons);
      
          if (buttons == SCREEN_LEFT_MOUSE_BUTTON) {
              //Left mouse button is pressed
              mouse_pressed = true;
          } else {
              if (mouse_pressed) {
                  //Left mouse button was released, add a cube
                  add_cube((float)pair[0], (float)pair[1]);
                  mouse_pressed = false;
              }
          }
      }

  5. If the event is a navigator event, determine the type of navigator event that occurred. Navigator events have codes associated with them, which are listed in the navigator.h library. In handleNavigatorEvent(), we retrieve the code for the event by using bps_event_get_code(). When a user swipes down from the top of the bezel, we call a function to clear all of the blocks from the screen. When a user closes the application, we set a flag that indicates that our application should exit out of the main application loop, clean itself up, and shut down.

    switch (bps_event_get_code(event)) {
    case NAVIGATOR_SWIPE_DOWN:
        clear_cubes();
        break;
    case NAVIGATOR_EXIT:
        shutdown = true;
        break;
    }

  6. If the event is a sensor event, invoke handleSensorEvent() to process any changes to the accelerometer on the BlackBerry PlayBook tablet. This function gets the current pitch and roll of the tablet from the sensor hardware and updates the gravity values accordingly. It also takes into account the current orientation of the device. The result is that blocks continue to fall vertically, no matter what the orientation of the tablet is.

    static void handleSensorEvent(bps_event_t *event) {
    	if (SENSOR_AZIMUTH_PITCH_ROLL_READING == bps_event_get_code(event)) {
    		float azimuth, pitch, roll;
    		float result_x = 0.0f, result_y = -1.0f;
    
    		sensor_event_get_apr(event, &azimuth, &pitch, &roll);
    
    		float radians = abs(roll) * M_PI / 180;
    		float horizontal = sin(radians) * 0.5f;
    		float vertical = cos(radians) * 0.5f;
    
    		if (pitch < 0) {
    			vertical = -vertical;
    		}
    		if (roll >= 0) {
    			horizontal = -horizontal;
    		}
    
    		//Account for axis change due to different starting orientations
    		if (orientation_angle == 0) {
    			result_x = horizontal;
    			result_y = vertical;
    		} else if (orientation_angle == 90) {
    			result_x = -vertical;
    			result_y = horizontal;
    		} else if (orientation_angle == 180) {
    			result_x = -horizontal;
    			result_y = -vertical;
    		} else if (orientation_angle == 270) {
    			result_x = vertical;
    			result_y = -horizontal;
    		}
    
    		set_gravity(result_x, result_y);
    	}
    }

Update the application data

We want to make sure that the positions of the blocks change each time the event-processing loop is executed; they are supposed to be falling blocks, after all! The update() function performs the math required to move the blocks.

  1. In update(), update the positions of the blocks on the screen. For each block on the screen, the update() function calculates the block's new position based on the current orientation of the tablet (indicated by the gravity values). We use a bit of algebra to calculate the position (using the equation for a line in the form of y = mx + b), and include a variety of cases for different circumstances. For example, we need separate cases for the intersection of a block with each edge of the screen. It's important to remember that update() doesn't actually draw the blocks in their new positions. The render() function does this step.

    static void update() {
    	//Update position of every cube
    	int i;
    	float m, b;
    
    	for (i = 0; i < num_boxes; i++) {
    		boxes[i].x += gravity_x * 5;
    		boxes[i].y += gravity_y * 5;
    
    		//y = mx + b
    		if ((gravity_x > 0.05) || (gravity_x < -0.05)) {
    			//General case, boxes are not falling vertically
    			m = gravity_y / gravity_x;
    			b = boxes[i].y - m * boxes[i].x;
    
    			if ((boxes[i].x > width) && (gravity_x > 0.0)) {
    				//Right edge
    				if ((b >= 0) && (b <= height)) {
    					//Intersection with x = 0
    					boxes[i].x = 0;
    					boxes[i].y = b;
    				} else if ((-b / m >= 0) && (-b / m <= width)) {
    					//Intersection with y = 0
    					boxes[i].x = -b / m;
    					boxes[i].y = 0;
    				} else if (((height - b) / m >= 0)
    						&& ((height - b) / m <= width)) {
    					//Intersection with y = APP_HEIGHT
    					boxes[i].x = (height - b) / m;
    					boxes[i].y = height;
    				} else {
    					//Corner case
    					boxes[i].x = 0;
    					boxes[i].y = 0;
    				}
    			} else if ((boxes[i].x + max_size < 0.0) && (gravity_x < 0.0)) {
    				//Left edge
    				//Perform similar intersection calculations here
        ...
    				}
    			} else if ((boxes[i].y > height) && (gravity_y > 0.0)) {
    				//Top edge
    				//Perform similar intersection calculations here
        ...
    				}
    			} else if ((boxes[i].y + max_size <= 0.0) && (gravity_y < 0.0)) {
    				//Bottom edge
    				//Perform similar intersection calculations here
        ...
    			}
    		} else {
    			//Special case, boxes are falling (almost) vertically so we can't describe m = -+ infinity effectively
    			if ((boxes[i].y > height) && (gravity_y > 0.0)) {
    				//Top edge
    				boxes[i].y = 0;
    			} else if ((boxes[i].y + max_size <= 0.0) && (gravity_y < 0.0)) {
    				//Bottom edge
    				boxes[i].y = height;
    			}
    		}
    	}
    }

Render the scene

Now that we've handled any events that can occur and updated the application data appropriately, all that's left is to draw the blocks in their appropriate positions on the screen.

  1. In render(), invoke glClear(). This function simply clears the screen to the background color that we specified when we initialized the Falling Blocks application.

    glClear(GL_COLOR_BUFFER_BIT);

  2. Provide the list of vertices of the blocks to OpenGL ES. We specify that each vertex consists of two floating-point values, and specify the list of vertices that we set up during initialization. OpenGL ES typically deals in 3-D coordinates, but we're only rendering in 2-D, so we don't need to worry about z-coordinates.

    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2, GL_FLOAT, 0, vertices);

  3. Draw each block in the appropriate position and with the appropriate size on the window surface. For each block, we set the color of the block, translate the block to its position on the screen, and scale the block. Finally, we draw the block on the screen. This code represents a typical OpenGL ES rendering pass.

    for (i = 0; i < num_boxes; i++) {
        glPushMatrix();
    
        glColor4f(boxes[i].color, 0.78f, 0, 1.0f);
        glTranslatef(boxes[i].x, boxes[i].y, 0.0f);
        glScalef(boxes[i].size, boxes[i].size, 1.0f);
    
        glDrawArrays(GL_TRIANGLE_STRIP, 0 , 4);
    
        glPopMatrix();
    }
    
    glDisableClientState(GL_VERTEX_ARRAY);

  4. Invoke bbutil_swap() to swap the window surface to the screen. This function is part of bbutil.h, and invokes the EGL function eglSwapBuffers() to display our content on the screen.

    bbutil_swap();