H5 game development: Snake and h5 Game Development
There are two classic ways to play a snake:
The first is what I first experienced in the handheld game machine when I was a child (accidentally exposing my age). The specific method is to clear customs after a snake eats a certain amount of food, and the speed will increase after customs clearance; the second is the game that Nokia installed on its mobile phone in 1997. Its gameplay is to stop eating food. I want to implement the second method.
MVC design pattern
Based on the snake classic, the author also uses a classic design Model: MVC (Model-View-Control) when implementing it ). The various statuses and data structures of the game are managed by the Model. The View is used to display changes to the Model. The interaction between the user and the game is completed by the Control (the Control provides various game API interfaces ).
Model is the core of the game and the main content of this article. View involves some performance issues. Control is responsible for business logic. The benefit of this design is that the Model is completely independent, and the View is the Model state machine. Both the Model and View are driven by Control.
Model
Take a look at a classic image of a snake.
Web Front-end/H5/javascript Learning Group: 250777811
Welcome to follow this public account → [web Front-end EDU] and learn the front-end together! You are welcome to leave a message to discuss and forward it together.
A snake has four key participants:
Stage ism * n
Matrix (two-dimensional array), the index boundary of the matrix is the wall of the stage, and the Members on the matrix are used to mark the location of food and snakes.
The empty stage is as follows:
[[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],]
Food (F) and snakes (S) appear on the stage:
[[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,F,0,0,0,0,0,0,0],[0,0,0,S,S,S,S,0,0,0],[0,0,0,0,0,0,S,0,0,0],[0,0,0,0,S,S,S,0,0,0],[0,0,0,0,S,0,0,0,0,0],[0,0,0,0,S,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],]
Because the operations on two-dimensional arrays are not as convenient as one-dimensional arrays, we use one-dimensional arrays as follows:
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,F,0,0,0,0,0,0,0,0,0,0,S,S,S,S,0,0,0,0,0,0,0,0,0,S,0,0,0,0,0,0,0,S,S,S,0,0,0,0,0,0,0,S,0,0,0,0,0,0,0,0,0,S,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,]
On the stage matrix, snakes and foods only map the two on the stage. Each of them has an independent data structure:
- A snake is a coordinate index linked list;
- Food is an index that points to the coordinates of the stage.
Snake Activity
There are three types of snake activity:
Mobile
What changes have taken place inside a snake while it is moving?
The snake chain table does two things during a moving process: Insert a new node to the header, and remove an old node at the end of the table. If an array is used to represent a snake chain table, the movement of the snake is the following pseudocode:
function move(next) {snake.pop() & snake.unshift(next); }
Is it appropriate to use arrays as a snake chain table? This is the question that I first thought about. After all, the arrayunshift & pop
It can seamlessly represent the movement of snakes. However, convenience does not mean good performance,unshift
The time complexity of inserting elements into an array is O (n ),pop
The time complexity of removing the elements at the end of the array is O (1 ).
The movement of a snake is a high-frequency action. If the algorithm complexity of an action is O (n) and the length of the snake is large, the game performance may be faulty. The greedy snake I want to implement is theoretically a long snake, so my reply in this article is ------ arrays are not suitable for use as a snake chain table.
A snake chain table must be a real linked list structure. The time complexity of deleting or inserting a node in the linked list is O (1). Using the linked list as the data structure of the snake chain table can improve the game performance. Javascript does not have a ready-made linked list structure. I wrote a Chain List class called Chain,Chain
Providedunshfit & pop
. The following Pseudo Code creates a snake chain table:
let snake = new Chain();
Food & Collision
The difference between "food" and "Collision" is that food hits "food" and hits "wall 」. The author believes that "food" and "Collision" are two branches of the three possible results of a snake moving at a time. Three possible outcomes of snake movement are: "forward", "eat", and "Collision 」.
Let's look back at the pseudocode of snake movement:
function move(next) {snake.pop() & snake.unshift(next); }
In the codenext
Indicates the index value of the grid to be entered by the snake header, only when this grid is0
When the snake can "forward", when this grid isS
Indicates "colliding" yourself. When this grid isF
It indicates eating.
Seems to have missed the wall? During the design process, the author did not design the wall in the matrix of the stage, but expressed the wall hit by indexing. Simply putnext === -1
It indicates the perimeter and hitting the wall.
The following pseudocode indicates the snake's overall activity process:
// B indicates hitting the wall let cell =-1 = next? B: zone [next]; switch (cell) {// food case F: eat (); break; // hit your case S: collision (S); break; // wall hitting case B: collision (B): break; // forward default: move ;}
Random feeding
Random feeding refers to the random selection of an index value on the stage to map the location of the food. This seems simple. You can directly write it like this:
// Pseudocode food = Math. random (zone. length)> 0;
If you consider the premise that the food is not overlapped with the snake body, you will find that the random code above does not guarantee that the food feed location does not overlap with the snake body. Because the security of this algorithm is gambling, it is called the "gambling algorithm 」. To ensure food safety, I have extended the algorithm:
// Pseudo-code function feed () {let index = Math. random (zone. length)> 0; // is the current position occupied return zone [index] = S? Feed (): index;} food = feed ();
Although the above Code can theoretically ensure the absolute security of food consumption, the author calls this algorithm a gambling algorithm 」, because the above algorithm has a fatal BUG ------ excessive recursion or endless loop.
To solve the above fatal problem, I have designed the following algorithms for random feeding:
// Pseudo-code function feed () {// number of unused spaces let len = zone. length-snake. length; // if (len = 0) return; // zone index let index = 0, // space counter count = 0, // The rnd blank lattice is the final position for food consumption. rnd = Math. random () * count> 0 + 1; // The total number of spaces while (count! = Rnd) {// The current grid is empty. Add a zone [index ++] ===0 & ++ count;} return index-1;} to the total count ;} food = feed ();
The average complexity of this algorithm is O (n/2 ). The O (n/2) complexity does not cause any performance problems because it is a low-frequency operation. However, I think the complexity of this algorithm is still a bit high. Looking back at the first "gambling algorithm", although the "gambling algorithm" is unreliable, it has an advantage-time complexity is O (1 ).
Reliable Probability of the gambling algorithm = (zone. length-snake. length)/zone. length.snake.length
Is a dynamic value, and its change range is:0 ~ zone.length
. The average Reliable Probability of the "gambling algorithm" is as follows:
"Gambling algorithm" average Reliable Probability = 50%
It seems that the gambling algorithm can still be used. So I re-designed an algorithm:
// Pseudo-code function bet () {let rnd = Math. random () * zone. length> 0; return zone [rnd] === 0? Rnd:-1;} function feed () {...} food = bet (); if (food =-1) food = feed ();
The average complexity of the new algorithm can be effectively reduced to O (n/4), and sometimes it takes some luck in life :).
View
In View, you can select a game rendering engine based on your preferences.PIXI
As a game rendering engine.
View has two main tasks:
That is to say, View is the process of restoring the design draft using the rendering engine. The purpose of this article is to introduce the implementation of "snake". How to use a rendering engine is not the scope of this article. The author wants to introduce "How to Improve rendering efficiency 」.
The snake that shows the Model in the View can be simply like the following pseudocode:
// Clear the snake View on The view. snake. clean (); model. snake. forEach (node) =>{ // create the snake node on the View let viewNode = createViewNode (node); // combine a new snake view. snake. push (viewNode );});
The time complexity of the above Code is O (n ). As mentioned above, snake movement is a high-frequency activity. We should try to avoid running O (n) code at a high frequency. To analyze three snake activities: "mobile", "food", and "Collision 」.
First, the Model has a "Collision", and the View should be directly paused in the rendering Model. The game is in the dead state, and the next cause is Control.
The snake (linked list) in the Model does two things during a "move" process: Insert a new node to the header, and remove an old node from the end of the table; the snake (Linked List) during a "food" operation, you only need to insert a new node to the header.
If you perform a differentiation check on the snake chain table of the Model in the View, if the View only incrementally updates the difference, the time complexity of the algorithm can be reduced to O (1 )~ O (2 ). The following is the optimized pseudocode:
Let snail KEA = model. snake, snail KEB = view. snake; // incrementally update the tail while (snakey B. length <= snail Kea. length) {headA = snail Kea. next (); // the header node matches if (headA. data = headB. data) break; // The else does not match {// Insert the header node if (snail Kea. HEAD === headA. index) {snail KEB. unshift (headA. data);} // Insert the second node else snail KEB to the snakb. insertAfter (0, headA. data) ;}} // incrementally update the header let tailA = snakey. last (), tailB; while (snail KEB. length! = 0) {tailB = snail KEB. last (); // The End Node matches if (tailA. data = tailB. data) break; // does not match else snakb. pop ();}
Control
Control mainly involves three tasks:
"Game-user interaction" refers to the APIs and events that need to be used in external game provision. The proposed APIs is as follows:
Name |
Type |
Deltail |
Init |
Method |
Game Initialization |
Start |
Method |
Start the game |
Restart |
Method |
Start the game again |
Pause |
Method |
Pause |
Resume |
Method |
Restore |
Turn |
Method |
Control the steering of a snake. For example: turn ("left ") |
Destroy |
Method |
Destroy a game |
Speed |
Property |
Speed of snake Movement |
The event is as follows:
Name |
Detail |
Countdown |
Hours |
Eat |
Food |
Before-eat |
Trigger before eating |
Gameover |
Game ended |
Events are uniformly mounted under game instances.event
Object.
Snake. event. on ("countdown", (time) => console. log ("remaining time:", time ));
"Driving Model" only does one thing ------ update the direction of the Model snake to the direction specified by the user. "Synchronous View and Model" is also relatively simple. Check whether the Model has been updated. If there is an update notification, View updates the game interface.
Conclusion
Add the source code of the snake Project
Web Front-end/H5/javascript Learning Group: 250777811
Welcome to follow this public account → [web Front-end EDU] and learn the front-end together! You are welcome to leave a message to discuss and forward it together.