The Sunday Blog: Scalable design and gaming

Part 2: Let’s build the game and gamble!

After a highly theoretic blog article last Sunday (huge thanks to all readers who made it through it), it’s now time to apply everything in practice. So, let’s build the project in the Nextion Editor. Those who fear copying and pasting a little less than 150 lines of commented code may download the ready-to-use .hmi file below. The pedagogic principle behind this blog is to teach how your HMI designs can be made scalable, able to run on different screen sizes and resolutions, with no or at least a minimum of modifications. Some component attributes (like x, y, h, and w) are immutable at runtime, so these need to be set “by hand” beforehand. But all what can be done in code will be done in code. At the same time, we need to reduce the CPU time for calculating and drawing the ball movement and the check for bouncing. Thus, we will make use of some pre-calculated helper vars.

For example, when we have a ball with a radius of 6 pixels, when does it touch the upper screen border? No, not when the ball’s center y-coordinate is 0, but when it equals the radius. So, for top and bottom bouncing, we can pre-calculate 0 + ball radius as the upper compare value for the ball’s center y, and page height – ball radius as the lower one. We’ll see more details below.

First, the page design

Rather easy-peasy, we’ll have to place…
– a full-height 5px wide vertical progress bar j0 at x=0 as score indicator
– a full-height 30px wide vertical slider h0 at x=10 as racket
– a full-height 30px wide vertical slider h1 at x=max-40 as racket
– a full-height 5px wide vertical progress bar j1 at x=max-10 as score indicator
– a timer tm0

Don’t care about colors and other attributes, they’ll all be set in code. The result should look like this:

Then, the program.s file

Here, we define all needed variables and set a few constants for the foreground and background colors and a highlight color for the score display progress bars.

// Declare variables and initialize the less dynamic ones
int ball_rad,ball_x,ball_y,new_x,new_y // ball coordinates (buffered)
int vect_x,vect_y // movement control
int tmp_b,tmp_t // tmp helper vars for bouncing at the sliders/rackets
int min_x,max_x,min_y,max_y // permanent boundary helper vars for quicker bouncing check
int min_out,max_out // permanent boundary helper vars for "ball out"
int bco=12678 // background color
int pco=65535 // foreground color
int hlt=63488 // highlight color
page 0 // start the game

In this file, we can’t yet start any calculation, since we need the page height and width for that, but these aren’t available as long as no page is loaded. An when a page is loaded, program.s does not longer execute. So, all that scaling and initializing will go into…

The page Post Initialization Event code

This is the place to put everything which depends on the page format: Setting the ball radius, the racket size, boundaries, colors, initial positions, and finally start the game by enabling the timer. We set the racket height (the .hig attribute of the sliders) to 1/4 of the page height and the maxval to the remainder. This has the advantage that the slider’s .val attribute always corresponds to the lower end of the racket (seen from the screen bottom) and (.val + .hig) to the upper end. Since the Nextion counts the y coordinates from the top, we’ll have to do subtractions from page.h later in the timer event code – that’s what the helpers tmp_b and tmp_t are for, to get the absolute y coordinates of the respective rackets to check if the ball will be hit back or if it goes out.

// Configure the page
page0.bco=bco
// Configure the timer
tm0.en=0
tm0.tim=50
// initialize variables depending on screen dimensions
ball_rad=page0.h/40
min_out=h0.x+ball_rad // left boundary if ball has not been hit back
min_x=h0.x+h0.w+ball_rad // left boundary if ball has been hit back
max_out=h1.x+h1.w-ball_rad // right boundary if ball has not been hit back
max_x=h1.x-ball_rad-1 // right boundary if ball has been hit back
min_y=ball_rad // top boundary
max_y=page0.h-ball_rad-1 // bottom boundary
// configure left racket
h0.bco=bco
h0.pco=pco
h0.hig=page0.h/4 // racket height
h0.minval=0
h0.maxval=page0.h-h0.hig // page height - racket height
h0.val=h0.maxval/2 // center position at start
// configure right racket
h1.bco=bco
h1.pco=pco
h1.hig=page0.h/4 // racket height
h1.minval=0
h1.maxval=page0.h-h1.hig // page height - racket height
h1.val=h0.maxval/2 // center position at start
// configure left result progress
j0.bco=bco
j0.pco=hlt
// configure right result progress
j1.bco=bco
j1.pco=hlt
// configure speed
vect_x=page0.w/2-h0.x-h0.w/24
vect_y=page0.h/80
// start over
j0.val=0
j1.val=0
ball_x=page0.w/2
ball_y=page0.h/2
cirs ball_x,ball_y,ball_rad,pco
delay=1000
tm0.en=1

Let’s move

The timer event code is responsible for moving, bouncing, and deciding if the ball is hit back or goes out, depending on the slider positions. This code should execute as quickly as possible to allow a visually fluid ball movement. After a ball goes out, the corresponding score counter is increased and then it’s thrown in again from the middle of the screen. If one of the score counters bars reaches full height, the game is over and starts again by reloading the page. Note that sometimes a combination of conditions has to be checked, the nesting of the if() clauses has been done in an optimized way, so that if the main condition is already not met, further conditions will not be evaluated to save CPU cycles at runtime.

//calculate movement step
new_x=ball_x+vect_x
new_y=ball_y+vect_y
// handle fails
if(new_x<=min_out)
{
  j0.val+=10 
  if(j0.val>=100)
  {
    // reset
    tm0.en=0
    page 0
  }else
  {
    // next round
    new_x=page0.w/2
    delay=500
  }
}else if(new_x>=max_out)
{
  j1.val+=10
  if(j1.val>=100)
  {
    //reset
    tm0.en=0
    page 0
  }else
  {
    // next round
    new_x=page0.w/2
    delay=500
  }
}
//handle y-bouncing
if(new_y<=min_y)
{
  new_y=min_y // adjust step to bounce with pixel precision even if step would go beyond
  vect_y=-1*vect_y // reverse y-direction
}else if(new_y>=max_y)
{
  new_y=max_y // see above about step adjustment
  vect_y=-1*vect_y // reverse y-direction
}
// handle x-bouncing
if(new_x<=min_x) 
{ 
  tmp_b=page0.h-h0.val
  tmp_t=tmp_b-h0.hig 
  // bounce only if racket in position 
  if(new_y>=tmp_t&&new_y<=tmp_b)
  { 
    new_x=min_x // adjust step to bounce with pixel precision even if step would go beyond
    vect_x=-1*vect_x // reverse x-direction
  }
}else if(new_x>=max_x)
{
  tmp_b=page0.h-h1.val
  tmp_t=tmp_b-h1.hig
  // bounce only if racket in position
  if(new_y>=tmp_t&&new_y<=tmp_b)
  {
    new_x=max_x // see above about step adjustment
    vect_x=-1*vect_x // reverse x-direction
  }
}
//undraw at old position
cirs ball_x,ball_y,ball_rad,bco
//draw at new position
cirs new_x,new_y,ball_rad,pco
ball_x=new_x
ball_y=new_y

Here, you may download the .hmi file, created for the 3.2″ Standard HMI display, but as described above, the code allows you to run it on all sizes, you’ll just have to adapt the height and position of the 4 screen components: gam_dev

Thank you for reading and happy gambling! 🙂