Memory Flip Game – a 2D game completed using Unity

Table of Contents

Preface

Game loop basics

Common game events

Immediate Mode GUI (IMGUI)

Entity-Component-System (ECS) concept

Model-View-Controller (MVC) concept

Game overview

Specific game implementation

Model part (Model)

Initialize model

Initialize various parameters as well as Table and List

View part (View)

Controller

The main logic implementation to determine whether to eliminate two cards

Find the number of elements and return the location

Determine if the game is over

Application of ECS (Entity Component System)

Conclusion

Code summary


Foreword

In this blog, I will introduce how to use unity and the concepts of Entity-Component-System (ECS) and Model-View-Controller (MVC) to make a simple 2D game – a memory flip game. In this process, we will understand the basic principles of the game loop, master the use of common game events and their execution order, learn to use simple operations and programming in Unity, and master the basic concepts of the game world. At the same time, he also has a certain in-depth understanding of ECS and MVC concepts.

Basic Principles of Game Loop

The game loop refers to the series of steps that the game engine performs every frame to update the game state and render the graphics. The following is the basic principle of the Unity game loop:

1. Initialization Phase:
Game engine initialization: Creation of game windows, initialization of graphics devices, etc.
Scene loading: Load game scenes, resources, etc.

2. Input Phase:
Handling input:Listen to player input, such as keyboard, mouse, touch screen, etc.
Update input state: Update the corresponding state in the game based on the input, such as controlling character movement.

3. Update Phase:
Update game state: Perform game logic, physics simulation, collision detection, etc.
Update game objects: Update the status of each object in the game, such as movement, rotation, animation, etc.
Handling game events: Trigger and handle events in the game, such as triggering event callbacks under specific conditions.

4. Render Phase:
Prepare for rendering: Set rendering targets, clear buffers and other preparations.
Rendering scene: Draw the visual representation of game objects to the screen, including geometric shapes, materials, lighting, etc.
Post-processing: Apply various post-processing effects such as color correction, blur, shadows, and more.
Present the screen: Display the rendering results on the screen to complete the rendering of one frame.

5. Repeat cycle:
The above steps will be executed sequentially in each frame, forming a game loop that continuously updates the game state and renders the screen.

Common game events

Now that we understand the basic principles of the game loop, we can explore some common game events and the order in which they are executed. Here is an overview of somecommon game events and the order in which they occur:

1. Start event:
Triggered after the game object is activated or the scene is loaded.
Used to initialize the state of game objects, set initial values, obtain references, etc.
Will only be executed once during the life cycle of the game object.

2. Update event:
Executed during the update phase of each frame.
Used to handle real-time updated operations such as game logic, input response, movement, and rotation.
The order of execution starts with each activated game object in the scene, and is executed sequentially in the order they appear in the scene.

3. FixedUpdate event:
Executed during the physics update phase of each frame.
Used to handle physics-related operations such as physics simulation, collision detection, etc.
The time interval is fixed, not affected by the frame rate, and is usually executed several times per second (for example, the default is 50 times per second).

4. LateUpdate (delayed update) event:
Executed after the Update event.
Used to handle operations that may affect other objects in the Update event.
For example, operations such as camera following and animation updates need to be performed after other objects have been updated.

5. OnCollisionEnter (collision entry) event:
Fired when a game object collides with another object.
Logic for handling collision events, such as triggering game effects, playing sound effects, etc.

6. OnTriggerEnter (trigger entry) event:
Fires when a game object enters the trigger.
Logic for handling trigger events, such as triggering tasks, switching scenes, etc.

7. OnGUI (Draw GUI) event:
Executed after the rendering phase.
Used to draw game interfaces, UI elements, etc.

It should be noted that different game events may differ in the order of execution, depending on the type of event and how it is registered. For example, events in a MonoBehaviour are executed in the order of the script, while physics events are tied to the update steps of the physics engine.

Instant mode GUI (IMGUI)

Immediate Mode GUI (IMGUI) is a simple and straightforward graphical user interface (GUI) system provided by the Unity engine. IMGUI allows you to write code directly in the game loop’s OnGUI event to create and update user interface elements.

IMGUI is based on the concept of immediate drawing. Whenever the OnGUI event is triggered, GUI elements will be drawn to the screen immediately. IMGUI uses a series of GUI functions to create different types of controls such as buttons, text boxes, sliders, etc. You can call these functions to set a control’s properties, handle user input, and respond to events.

Here’s a basic example of using IMGUI:

using UnityEngine;

public class MyGUI : MonoBehaviour
{
    private string playerName = "Player";
    private int score = 0;

    private void OnGUI()
    {
        //Create a label to display player name and score
        GUI.Label(new Rect(10, 10, 200, 20), "Player Name: " + playerName);
        GUI.Label(new Rect(10, 30, 200, 20), "Score: " + score);

        //Create a button to increase the score when clicked
        if (GUI.Button(new Rect(10, 60, 80, 20), "Add Score"))
        {
            score + = 10;
        }

        //Create a text box for entering the player name
        playerName = GUI.TextField(new Rect(10, 90, 120, 20), playerName);
    }
}

It should be noted that IMGUI is an immediate drawing method, and all GUI elements will be redrawn every frame, which may have a certain impact on performance. Therefore, for complex GUI interfaces or situations that require frequent updates, Unity recommends using new UI systems (such as Canvas and RectTransform) to build user interfaces.

Entity-Component-System (ECS) Concept

ECS (Entity Component System) is a software architecture pattern for building games and applications. Its core idea is to split game objects (Entities) into components (Components) and systems (Systems) to provide a more efficient, scalable and maintainable development method.

In traditional object-oriented programming, game objects are usually represented by a single class that contains various properties and behaviors. However, when game objects become complex, this design pattern can lead to class bloat and unmanageability. ECS provides a more flexible development approach by separating data and behavior.

The following are the three core concepts of ECS:

1. Entity:
Entities represent objects in the game, which can be characters, enemies, props, etc.
The entity itself is just an identifier or unique identification (ID) and does not contain any data or behavior.

2. Component:
Components are the data portion of an entity and are used to describe the characteristics and properties of the entity.
Components are pure data, usually structures or classes, containing only properties and data, without any behavior.
An entity can have multiple components, and different components can be used to describe different aspects of the entity, such as position, rendering, collision, etc.

3. System:
A system is the behavioral part that handles components and is used to operate and process entities with a specific set of components.
The system performs logic and operations based on the component’s data, such as updating position, rendering graphics, handling collisions, etc.
Systems are self-contained and multiple systems can be created as needed to handle different types of components.

The core idea of ECS is to decouple entities into components and systems so that the system can efficiently handle entities with the same set of components, improving performance and scalability. In addition, ECS supports data-driven design, making it easier for developers to optimize and parallelize their code to take advantage of modern multi-core processors.

Model-View-Controller (MVC) Concept

Model-View-Controller (MVC) is a software design pattern used to organize and manage the structure and interaction logic of an application. It divides the application into three main components: Model, View and Controller. Each component has different responsibilities and functions to achieve separation of concerns and improve code maintainability.

Here are the functions and responsibilities of each component in the MVC pattern:

1. Model:
Models represent the application’s data and business logic.
It is responsible for managing the reading, storage, processing and verification of data.
Models usually include data structures, database operations, business rules, algorithms, etc.

2. View:
The view is responsible for presenting data from the model to the user and receiving input from the user.
It is usually part of a user interface (UI), such as a graphical interface, web page, or command line interface.
A view is a visual representation of model data, but it is not responsible for processing the data or logic itself.

3. Controller:
The controller acts as an intermediary between the model and the view, coordinating the interactions between them.
It receives input or requests from the user and updates models and views as needed.
The controller handles user events, invokes appropriate model operations, and updates the view in response.

The core idea of the MVC pattern is to separate concerns so that each component focuses on its own task. The model is responsible for data and business logic, the view is responsible for the presentation of the user interface, and the controller is responsible for processing user input and coordinating the interaction between the model and the view.

Game Overview

Okay, with the previous basic knowledge in mind, we can start making our 2D mini game. The game we are preparing to make now is a memory flip game. The specific gameplay is: when starting the game, all cards cannot see the numbers. Only when we click, the cards will flip over, that is, the cards can be seen. The numbers on the cards. When we click on the two cards to flip, if the numbers on the two cards are not equal, the cards will be flipped over again; if the numbers on the two cards are equal, then the two cards will be disappear. Keep clicking like this to make the cards disappear two by two until all the cards are eliminated, that is, the game is over. You can click Restart to restart the game.

Let’s take a look at the implementation of this game first!

Let’s take a look at our game interface first:

Here is a link to our game showcase:

Memory Card Game

Specific game implementation

Model

Initialization model

 //1. Model part
    //Initialize model
    private List<int> numbers;
    private int[,] table;
    private int line_column=6;
    private int[,] Is_Click;
    private bool CanClick=true;

Numbers: This is a List, used to store numbers that may appear in the table, and to achieve matching between numbers to achieve elimination.

table: This is a 2D integer array that represents the number at each position in the table.

line_column: Indicates the rows and columns of this matrix (table). In order to achieve pairwise elimination between numbers, this variable should be an even number.

Is_Click: This is a 2D integer array, indicating which position in the table was clicked, and also indicating which position of the number has been eliminated.

CanClick: This is a Boolean variable, which means that when there are two cards clicked, no other cards can be clicked until the two cards are flipped over again.

Initialize various parameters and Table and List

 //Initialize various parameters and List and Table
    void Init(){
        //First initialize table and List
        table=new int[line_column,line_column];
        Is_Click=new int[line_column,line_column];
        numbers=new List<int>();

        //Initialize the numbers into the list
        for(int i=1;i<=line_column*line_column/2;i + + ){
            numbers.Add(i);
            numbers.Add(i);
        }

        //Shuffle randomly and randomly disrupt the data in the list
        System.Random random = new System.Random();

        for (int i = 0; i < numbers.Count - 1; i + + ){
            int j = random.Next(i, numbers.Count);
            int temp = numbers[i];
            numbers[i] = numbers[j];
            numbers[j] = temp;
        }

        //Convert the data in the list to the table
        //At the same time, initialize all data of Is_Click to 0
        for(int i=0;i<line_column;i + + ){
            for(int j=0;j<line_column;j + + ){
                table[i,j]=numbers[i*line_column + j];
                Is_Click[i,j]=0;
            }
        }
    }

We need to initialize table, Is_Click and numbers, first assign values to numbers, then randomly scramble the data in this List, and finally map the numbers of this List to the table. At the same time, we also need to initialize all the data of Is_Click as 0.

View

 //2. View View part
    void OnGUI(){
        GUI.Box(new Rect(255,50,70*line_column,70*line_column),"");
        if(GUI.Button(new Rect(215 + line_column/2*70,65 + line_column*70,100,30),"Restart")){
            Init();
        }
        for(int i=0;i<line_column;i + + ){
            for(int j=0;j<line_column;j + + ){
                if(Is_Click[i,j]==0 & amp; & amp;GUI.Button(new Rect(255 + j*70,50 + i*70,70,70),"") & amp; & amp;CanClick){
                        Is_Click[i,j]=1;
                        StartCoroutine(Is_Fade_Away());
                }
                else{
                    if(Is_Click[i,j]==1){
                        GUI.Button(new Rect(255 + j*70,50 + i*70,70,70),table[i,j].ToString());
                    }
                }
            }
        }
        if(GameOver()){
            GUI.Box(new Rect(260,50,70*line_column,70*line_column),"\\
\\
\\
\\
\\
\\
\\
\\
Congratulations!\\
 ");
        }
    }

This is the main function of OnGUI. We first need to create a Box, and then generate a Button in this Box to display the string “Restart!” to implement the function of restarting the game. Then start to generate a matrix of line_column*line_column, and generate line_column*line_column Buttons. Then start to make logical judgments. When the card is clicked and Is_Click is 0, the value of this Is_Click is assigned to 1, and then all cards with Is_Click of 1 are flipped over to display the numbers. When two cards are clicked, you need to judge whether the two cards are equal. If they are equal, assign the value of Is_Click to 2. If they are not equal, set the value of the two cards to the location where the two cards are located. Is_Click is set to 0. Finally, until the Is_Click value of the corresponding positions of all cards is 2, that is, all cards have been eliminated, then the game ends and “Congratulations!” is printed.

Controller

The main logic implementation to determine whether to eliminate two cards

 //3. Control the Components/Controller part
    //The main logic implementation to determine whether to eliminate two cards
    IEnumerator Is_Fade_Away(){
        CanClick=false;
        List<int> counts=Find_element(1);
        if(counts.Count==2){
            int i_1=counts[0]/line_column;
            int j_1=counts[0]-i_1*line_column;
            int i_2=counts[1]/line_column;
            int j_2=counts[1]-i_2*line_column;

            if(table[i_1,j_1]==table[i_2,j_2]){
                yield return new WaitForSeconds(0.5f);
                Is_Click[i_1,j_1]=2;
                Is_Click[i_2,j_2]=2;
            }
            else{
                yield return new WaitForSeconds(0.5f);
                Is_Click[i_1,j_1]=0;
                Is_Click[i_2,j_2]=0;
            }
        }
        CanClick=true;
    }

To determine whether a card has been eliminated, you first need to obtain the position of the element with a value of 1 in Is_Click, and then determine how many elements are 1. If there is only one 1, no processing will be performed. If there are 2 1s, then a judgment will be made. Whether the numbers in these two positions are equal. If they are equal, the value of Is_Click is assigned to 2; if they are not equal, the value of Is_Click is reassigned to 0. At the same time, we noticed that in order to limit clicking on two cards, that is, we can only see the numbers of two cards at the same time, we set CanClick to false when entering this function, and then exit this function. When setting CanClick to true. At the same time, in order to increase the memory time when the two cards are not equal, we will use the coroutine method to make the function wait here for 0.5 seconds.

Find the number of elements and return the location

 //Find the number of elements with a value of 1 in Is_Click and return the position
    List<int> Find_element(int element){
        List<int> counts=new List<int>();
        for(int i=0;i<line_column;i + + ){
            for(int j=0;j<line_column;j + + ){
                if(Is_Click[i,j]==element){
                    counts.Add(i*line_column + j);
                }
            }
        }
        return counts;
    }

This function is used to find how many 1’s there are in Is_Click and return the position of each 1. We store these positions in a List and return this List as the return value.

Judge whether the game is over

 //Determine whether the game is over
    bool GameOver(){
        bool temp=true;
        for(int i=0;i<line_column;i + + ){
            for(int j=0;j<line_column;j + + ){
                if(Is_Click[i,j]!=2){
                    temp=false;
                    break;
                }
            }
        }
        return temp;
    }

The game will end if and only if all the data in Is_Click is 2. Even if one is not 2, it will return false.

Application of ECS (Entity Component System)

Although ECS mode is not explicitly implemented in the above code, we can consider how to introduce it into the game. For example, we can create a Card component that contains the card’s value, status, and other information, and then write a system to handle the flipping and matching of cards. This way we can better separate game logic and data.

Conclusion

The above is a 2D small game we made using unity and combining the concepts of ECS and MVC. The three parts of MVC are clear and detailed, and they are mutually restricted and connected. Through this small game, we can learn more clearly the specific meaning and application of ECS and MVC concepts, and also have a deeper understanding and help for getting started with Unity. So, let’s get started together and use unity to create a game that belongs to you!

Code Summary

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConcerationGame : MonoBehaviour
{

    //1.Model part
    //Initialize model
    private List numbers;
    private int[,] table;
    private int line_column=6;
    private int[,] Is_Click;
    private bool CanClick=true;

    // Start is called before the first frame update
    void Start()
    {
        Init();
    }

    //Initialize various parameters and List and Table
    void Init(){
        //First initialize table and List
        table=new int[line_column,line_column];
        Is_Click=new int[line_column,line_column];
        numbers=new List<int>();

        //Initialize the numbers into the list
        for(int i=1;i<=line_column*line_column/2;i + + ){
            numbers.Add(i);
            numbers.Add(i);
        }

        //Shuffle randomly and randomly disrupt the data in the list
        System.Random random = new System.Random();

        for (int i = 0; i < numbers.Count - 1; i + + ){
            int j = random.Next(i, numbers.Count);
            int temp = numbers[i];
            numbers[i] = numbers[j];
            numbers[j] = temp;
        }

        //Convert the data in the list to the table
        //At the same time, initialize all data of Is_Click to 0
        for(int i=0;i<line_column;i + + ){
            for(int j=0;j<line_column;j + + ){
                table[i,j]=numbers[i*line_column + j];
                Is_Click[i,j]=0;
            }
        }
    }


    //2. View View part
    void OnGUI(){
        GUI.Box(new Rect(255,50,70*line_column,70*line_column),"");
        if(GUI.Button(new Rect(215 + line_column/2*70,65 + line_column*70,100,30),"Restart")){
            Init();
        }
        for(int i=0;i<line_column;i + + ){
            for(int j=0;j<line_column;j + + ){
                if(Is_Click[i,j]==0 & amp; & amp;GUI.Button(new Rect(255 + j*70,50 + i*70,70,70),"") & amp; & amp;CanClick){
                        Is_Click[i,j]=1;
                        StartCoroutine(Is_Fade_Away());
                }
                else{
                    if(Is_Click[i,j]==1){
                        GUI.Button(new Rect(255 + j*70,50 + i*70,70,70),table[i,j].ToString());
                    }
                }
            }
        }
        if(GameOver()){
            GUI.Box(new Rect(260,50,70*line_column,70*line_column),"\\
\\
\\
\\
\\
\\
\\
\\
Congratulations!\\
 ");
        }
    }


    //3. Control the Components/Controller part
    //The main logic implementation to determine whether to eliminate two cards
    IEnumerator Is_Fade_Away(){
        CanClick=false;
        List<int> counts=Find_element(1);
        if(counts.Count==2){
            int i_1=counts[0]/line_column;
            int j_1=counts[0]-i_1*line_column;
            int i_2=counts[1]/line_column;
            int j_2=counts[1]-i_2*line_column;

            if(table[i_1,j_1]==table[i_2,j_2]){
                yield return new WaitForSeconds(0.5f);
                Is_Click[i_1,j_1]=2;
                Is_Click[i_2,j_2]=2;
            }
            else{
                yield return new WaitForSeconds(0.5f);
                Is_Click[i_1,j_1]=0;
                Is_Click[i_2,j_2]=0;
            }
        }
        CanClick=true;
    }

    //Find the number of elements with a value of 1 in Is_Click and return the position
    List<int> Find_element(int element){
        List<int> counts=new List<int>();
        for(int i=0;i<line_column;i + + ){
            for(int j=0;j<line_column;j + + ){
                if(Is_Click[i,j]==element){
                    counts.Add(i*line_column + j);
                }
            }
        }
        return counts;
    }

    //Determine whether the game is over
    bool GameOver(){
        bool temp=true;
        for(int i=0;i<line_column;i + + ){
            for(int j=0;j<line_column;j + + ){
                if(Is_Click[i,j]!=2){
                    temp=false;
                    break;
                }
            }
        }
        return temp;
    }
}