📜 ⬆️ ⬇️

Mechanics of casual games

At Habrahabr periodically attempts are made to describe the process of igrodelaniya from various sides - from the incarnation of 3D-graphics to the creation of network protocols. These topics are certainly important, but rather narrow. In this article I will try to use a wider approach - I will consider the principle of creating a game engine for a so-called. casual games. The described mechanics are quite suitable for creating all sorts of pakmanov, arkanoids, platformers, etc. The process description will be exemplified by a primitive scrolldown shooter (from nostalgic feelings to Zybex and Xevious ) - we fly around the field, knocking down meteorites. Tool - Qt.

Immediately make a reservation that there is no beauty and completeness in the code. The classes are primitive and repeat the code, the functions are not optimal, the graphics are ugly , but it all puffs and turns. This is a base from which to work further. Experienced programmers - flipping through a cup of something hot, beginners or pouring into the subject will probably give food for thought. Getting started.

Application cycle


In order to choose the right way to organize the program, you need to decide on the participants of the main cycle. In any casual (and most non-casual) game, there are at least three of them:

Other points are also possible - animation manager, artificial intelligence, etc. For example, these three are enough for us.

What kind of participants are these?
')
The clock of the gameplay is tied to the timer ... uh ... the clock of the gameplay. It controls the movement of objects on the playing field. Its main purpose is to ensure the integrity of the gameplay and its sameness . This is very important - not only to ensure that the speed of the game does not depend on the performance of the computer, but also to ensure synchronization when playing over the network.

The continuity simulator is auxiliary functions, the main purpose of which is to ensure that nothing important happens between the calls of the game process generator. For example, consider the following game moment:
image

To the left and to the right there are two consecutive calls of the clock generator of the game process. Suppose that the speed of the yellow circle = 3. The distance between the circle and the rectangle, as can be seen from the figure, = 2. It turns out that the circle and the rectangle will not collide if they are not helped. This help and provides simulator continuity.

Rendering the scene - everything seems to be clear. It is a separate item, because:

Possible ways of organizing cycles


Immediately comes to mind the idea to make a separate thread for each of the participants. However, this approach is not optimal, since:

Therefore, we will do the threads, but somewhat differently - a separate thread for processing messages from the OS (drawing, polling the keyboard), and a separate thread for the gameplay, in which all three participants are called.
With drawing and polling the keyboard, everything is clear - it's just a stream of the main form of the application. We will deal with the flow of gameplay.

Gameplay flow


The flow structure is shown in the figure:
image

Now a little code with explanations.
To begin, set the frequency. Roughly speaking, how many ms should pass between calls for processing logic, rendering:
Copy Source | Copy HTML
  1. // Clock Generator: FREQ - Logic, FPS - Rendering
  2. const int FREQ = 1000/40; // 1000 / FPS
  3. const int MAX_FPS = 1000/180;


But the code of the main loop - with the call of all participants, checks on time, etc.
Copy Source | Copy HTML
  1. while ( true )
  2. {
  3. qint64 time_cur_tick = QDateTime :: currentMSecsSinceEpoch ();
  4. int numLoops = 0 ;
  5. bool ft = true;
  6. while ( time_prev_tick <time_cur_tick && numLoops <MAX_LOOPS )
  7. {
  8. // call logic
  9. w-> UpdateLogic ( 1 / FREQ );
  10. numLoops ++;
  11. if ( ft )
  12. {
  13. ft = false;
  14. last_freq = time_cur_tick;
  15. }
  16. time_prev_tick + = FREQ;
  17. // Update time_cur_tick for more accurate clocking
  18. time_cur_tick = QDateTime :: currentMSecsSinceEpoch ();
  19. }
  20. time_tmp = QDateTime :: currentMSecsSinceEpoch ();
  21. w-> SimulateConsistLogic ( (float ) ( time_tmp - last_freq ) / FREQ);
  22. time_tmp = QDateTime :: currentMSecsSinceEpoch ();
  23. if ( time_tmp - time_lastrender> = MAX_FPS &&
    w-> paint_mx.tryLock ( ))
  24. {
  25. time_lastrender = time_tmp;
  26. float freq_bit = 0 ;
  27. if ( time_tmp! = last_freq )
  28. freq_bit = ( float ) ( time_tmp - last_freq ) / FREQ;
  29. emit signalGUI ( freq_bit );
  30. w-> paint_mx.unlock ();
  31. }
  32. }


(note - if you look at the source code - everything is a bit more complicated there. There is counting frames per second, displaying debug information, etc.)

Surely the question arose - why the rendering functions and the continuity simulator know the time that has passed since the last update of the game logic? Everything is simple - in order to calculate the instantaneous state of the scene, and correctly process it and display it on the screen. To save resources, calling the continuity simulator can also transfer the time of its last call.

How it all works


In our example, three types of objects:

For them the corresponding classes (CShip, CBullet, CMeteorite) are made For bullets and meteorites, QVector storage containers are specified.
To handle user input, an array of “driving directions” has been created and the keyReleaseEvent and keyPressEvent functions have been redefined:
keyReleaseEvent checks whether the array of keystrokes contains a released key, and deletes it if present.
keyPressEvent accordingly puts the pressed key into the array of the pressed keys (if it is not there). Processing of this array occurs as a function of the clock generator of the game. In the same place there are movements of game objects, calculation of inertia when the ship is moving, the creation of meteorites:
Copy Source | Copy HTML
  1. void MainWindow :: UpdateLogic ( float ftime )
  2. {
  3. float speed = 2 ;
  4. for ( int i = 0; i <m_dir.size ( ); i ++)
  5. {
  6. if ( m_dir [i] == MainWindow :: UP )
  7. actor1.adjust Direction ( QVector2D (0, -speed ));
  8. if ( m_dir [i] == MainWindow :: DOWN )
  9. actor1.adjust Direction ( QVector2D (0, speed ));
  10. if ( m_dir [i] == MainWindow :: LEFT )
  11. actor1.adjust Direction ( QVector2D (-speed, 0 ));
  12. if ( m_dir [i] == MainWindow :: RIGHT )
  13. actor1.adjust Direction ( QVector2D (speed, 0 ));
  14. if ( m_dir [i] == MainWindow :: SPACE &&
    m_allowbullet == 0 )
  15. {
  16. m_bullets.push_back ( CBullet (actor1.getX ( ), actor1.getY () - 1 , QVector2D ( 0, -15 )));
  17. qDebug ( QString (“Added bullet. Pos% 1” ) .arg ( m_bullets.size ( ) - 1 ) .toAscii ());
  18. m_allowbullet = 5 ;
  19. fired ++;
  20. }
  21. }
  22. actor1.step Direction ();
  23. bool dir_touched = false;
  24. for ( int i = 0; i <m_dir.size ( ); i ++)
  25. {
  26. if ( m_dir [i]! = MainWindow :: SPACE )
  27. {
  28. dir_touched = true;
  29. break;
  30. }
  31. }
  32. if ( ! dir_touched )
  33. {
  34. m_allowmove = 0 ;
  35. float inertia = 0 . 5 ;
  36. if ( actor1.getSpeed ​​( ) < 0 . 5 )
  37. inertia = 1 ;
  38. actor1.adjustSpeed ​​( inertia );
  39. }
  40. for ( int i = 0; i <m_bullets.size ( ); i ++)
  41. m_bullets [ i ] .step Direction ();
  42. for ( int x = 0; x <m_enemies1.size ( ); x ++)
  43. m_enemies1 [x] .step Direction ();
  44. CheckGameRules ();
  45. if ( m_enemies1.size ( ) <max_enemies)
  46. {
  47. CMeteorite meteo ( mrand (field_ident + CMeteorite :: meteo_size,
    field_ident + field_w - CMeteorite :: meteo_size ),
  48. -mrand ( 0, 20 ),
  49. QVector2D ( 0, 1 ));
  50. while ( true )
  51. {
  52. int i = 0 ;
  53. while ( i <m_enemies1.size ( ))
  54. {
  55. if ( meteo.getBoundsT ( ) .intersects ( m_enemies1 [i] .getBoundsT ( )))
  56. break;
  57. i ++;
  58. }
  59. if ( i == m_enemies1.size ( ))
  60. break;
  61. meteo = CMeteorite ( mrand (1, 100 ), -mrand ( 0, 20 ),
  62. QVector2D ( 0, 1 ));
  63. }
  64. m_enemies1.push_back ( meteo );
  65. }
  66. UpdateBullet ();
  67. }

The CheckGameRules function checks the game rules - who crashed into whom, who went beyond what, and so on. By the way, in 2D this is all very conveniently done by the functions of the QPolygon, QRect classes and their ilk.

Copy Source | Copy HTML
  1. void MainWindow :: CheckGameRules ( const float ftime )
  2. {
  3. QRect field_rect ( field_ident, field_ident,
    field_w,
    field_h );
  4. for ( int i = 0; i <m_bullets.size ( ); i ++)
  5. {
  6. CBullet blt = m_bullets [ i ];
  7. float tx = 0 , ty = 0 ;
  8. blt.getTickCoords ( ftime, tx, ty );
  9. blt.setX ( tx );
  10. blt.setY ( ty );
  11. if ( ! field_rect.contains (m_bullets [i] .getX ( ), m_bullets [ i ] .getY ()))
  12. {
  13. m_bullets.remove ( i-- );
  14. }
  15. else
  16. {
  17. for ( int j = 0; j <m_enemies1.size ( ); j ++)
  18. {
  19. CMeteorite enm = m_enemies1 [j];
  20. float etx = 0 , ety = 0 ;
  21. enm.getTickCoords ( ftime, etx, ety );
  22. enm.setX ( etx );
  23. enm.setY ( ety );
  24. if ( blt.checkCollision (enm.getBodyT ( )))
  25. {
  26. m_enemies1.remove ( j-- );
  27. m_bullets.remove ( i-- );
  28. score ++;
  29. break;
  30. }
  31. } // for
  32. }
  33. }
  34. for ( int j = 0; j <m_enemies1.size ( ); j ++)
  35. {
  36. CMeteorite enm = m_enemies1 [j];
  37. if ( ! field_rect.contains (enm.getBoundsT ( )) &&
  38. field_rect. bottomRight () .y () <enm.getBoundsT (). topLeft () .y ())
  39. {
  40. m_enemies1.remove ( j-- );
  41. }
  42. if ( actor1.checkCollision (enm.getBodyT ( )))
  43. {
  44. m_enemies1.remove ( j-- );
  45. hits ++;
  46. }
  47. }
  48. if ( ! field_rect.contains (actor1.getBoundsT ( ), true))
  49. {
  50. while ( field_rect.x ( )> = actor1.getBoundsT (). left ())
  51. actor1.setX ( actor1.getX ( ) + 1 );
  52. while ( field_rect.x ( ) * 2 + field_rect. width () <= actor1.getBoundsT (). x () + actor1.getBoundsT (). width ())
  53. actor1.setX ( actor1.getX ( ) - 1 );
  54. while ( field_rect.top ( )> = actor1.getBoundsT (). top ())
  55. actor1.setY ( actor1.getY ( ) + 1 );
  56. while ( field_rect.y ( ) * 2 + field_rect. height () <= actor1.getBoundsT (). y () + actor1.getBoundsT (). height ())
  57. actor1.setY ( actor1.getY ( ) - 1 );
  58. actor1.s top ();
  59. }
  60. }


Accordingly, the call simulator continuity is simple to ugliness. With only a small step, we check the game logic:
Copy Source | Copy HTML
  1. void MainWindow :: SimulateConsistLogic ( float ftime )
  2. {
  3. for ( float bt = 0; bt <ftime; bt = bt + 0.1 )
  4. {
  5. CheckGameRules ( bt );
  6. }
  7. }


Rendering renders the playing field and calls Draw () of all objects with the parameter of the current offset from the last call of the clock generator of the game process. Plus the output of service information:
Copy Source | Copy HTML
  1. void MainWindow :: Render ()
  2. {
  3. QPainter qpainter ( this );
  4. const int bgw = 2 ;
  5. qpainter.setPen ( QPen (Qt :: black, bgw ));
  6. qpainter.setBrush ( QBrush (Qt :: darkGray ));
  7. qpainter.drawRect ( field_ident, field_ident,
    field_w + field_ident,
    field_h + field_ident );
  8. for ( int i = 0; i <m_bullets.size ( ); i ++)
  9. {
  10. CBullet blt = m_bullets [ i ];
  11. blt.Draw ( qpainter, freq_bit );
  12. }
  13. for ( int i = 0; i <m_enemies1.size ( ); i ++)
  14. {
  15. CMeteorite enm = m_enemies1 [ i ];
  16. enm.Draw ( qpainter, freq_bit );
  17. }
  18. actor1.Draw ( qpainter, freq_bit );
  19. QPalette pal;
  20. qpainter.setBrush ( pal.brush (QPalette :: Window ));
  21. qpainter.setPen ( QPen (pal.color (QPalette :: Window ), 1 ));
  22. qpainter.drawRect ( field_ident - bgw / 2, 0,
    field_w + field_ident + bgw / 2,
    field_ident - bgw );
  23. qpainter.setPen ( QPen (Qt :: black, bgw ));
  24. qpainter.setBrush ( QBrush (Qt :: darkGray, Qt :: NoBrush ));
  25. qpainter.drawRect ( field_ident, field_ident,
    field_w + field_ident,
    field_h + field_ident );
  26. ui-> label_freq-> setText ( QString ("% 1" ) .arg ( freq ) .toAscii ());
  27. ui-> label_fps-> setText ( QString ("% 1" ) .arg ( fps ) .toAscii ());
  28. ui-> label_speed-> setText ( QString ("% 1" ) .arg ( actor1.getSpeed ​​( )) .toAscii ());
  29. ui-> label_score-> setText ( QString ("% 1" ) .arg ( score ) .toAscii ());
  30. ui-> label_fired-> setText ( QString ("% 1" ) .arg ( fired ) .toAscii ());
  31. ui-> label_hits-> setText ( QString ("% 1" ) .arg ( hits ) .toAscii ());
  32. }


Actually, the rest is trivial programming. The skeleton of the application is disassembled, and the implementation details can be viewed in the attached source codes. As a result - the appearance of what I got:
image
Sources here . We fly by arrows, we shoot with space.
Sources on githaba .

Source: https://habr.com/ru/post/124495/


All Articles