Let's use Unity to make a 2D pixel boss battle

From a personal point of view, "Dead Cells" has many features that I can't put it down: excellent operating feel, fragmented plot, varied random maps, a variety of distinctive enemies, rich equipment (skills) system—and Wonderful and cool Boss battle. Whether it's the giant boss in the guardian's residence or the small boss time assassin in the clock room, I can feel a very ingenious sense of oppression in the process of fighting them. This right amount of oppression makes me highly focused. Constantly learning from failure - until the moment the boss is defeated, the heart flow is transformed into an unprecedented sense of accomplishment.


Based on this idea, I came up with the idea of ​​writing a small tutorial, let's make a simple Boss battle together.

First of all, think about what is needed in a Boss battle?

A Boss monster waiting for the player to hit (rou) and defeat (lin);

a character played by the player;

And a scene that doesn't hurt the eyes.

Of course, each part of it also has some subdivided components. Let's create our Boss battle step by step.

The first is to create a new Unity2D project, and then import the prepared materials into it. Here you can go to the Unity store or the itch.io website to download the materials you need.

https://assetstore.unity.com/

https://itch.io/

The material I chose here is a flame centipede. After importing the materials, we found that most of the materials put a complete set of animations in one image, and what we have to do is to "cut" each sprite image in an integrated image. The specific operation as follows: 

 

PS: It is not recommended to use the Automatic mode for cutting. The size of the cut pictures may vary, which may cause the center point of each sprite to be different. If the material you are looking for is arranged like mine, you can use the Grid By Cell Count mode to cut it to ensure that the size of each piece is consistent with the center point.

Well, after we have prepared all the materials according to the above process, we will make each animation and name it for later use. Now choose a sprite image as the entity of the Boss in the game, and let's hang various components for the Boss in advance. 

 

PS: The reason for choosing PolygonCollider2D (polygon collider) here is that the Boss here is not a conventional shape, and PolygonCollider2D supports manual dragging out of the shape we want, but if there is a need to change the collider in the game later It will be very troublesome. Therefore, here you can also use several colliders with fixed shapes to stitch together the general shape of the Boss, so as to avoid the above problems.

What kind of behavior should there be in Boss AI?

Just turn your head and you can think of standby, injury, attack, death, movement, etc. This is the state that should exist in the simplest enemy AI. Because what we are doing today is not ordinary miscellaneous soldiers, at least there is a title: Boss (I also have dignity!). So here the states are divided into: standby, injured, death, collision skills, fireball skills, and fire column skills.

Now let's consider what the switching conditions between the states are? What kind of process is it?

After thinking about it, you can go directly to the flow chart:

After you figure out the process, you can start writing a finite state machine (FSM). Since the content of this project is not much, I chose to use enum (enumeration class) to list all the states of Boss first, and then use the switch case statement to distinguish the corresponding methods between different states. Cut the nonsense and go to the code.

public enum BossState//List all states
{     FireBall,     FirePillar,     Dash,     Idle,     BeHit,     Death, }






BossState state;

void Awake
{     state=BossState.Idle;//Set the initial state of Boss to Idle }

void Update
{
    switch (state)
    {
      switch (state)
        {
            case BossState.FireBall:
                {
                    FireBallSkill();
                    break;
                }
            case BossState.FirePillar:
                {
                    FirePillarSkill();

                    break;
                }
            case BossState.Dash:
                {
                    DashSkill();
                    break;
                }
            case BossState.Idle:
                {
                    IdleProccess();
                    break;
                }
            case BossState.BeHit:
                {
                    BeHitProccess();
                    break;
                }
            case BossState.Death:
                {
                   DeathProccess();
                    break;
                }
    }
}


Like this, we have set up the shelf of the simplest finite state machine, and then we only need to write the logic in the corresponding state.

PS: The advantage of this finite state machine is that it is simple and easy to understand, but at the same time the disadvantage is also obvious-putting all the codes together, whether it is later maintenance modification or adding new content, it will be very troublesome. So we can subdivide the state machine into: state machine system, state base class, the actual script mounted on the object, use the inherited state base class to write different states, the state machine system controls the state method call and add and delete, and then in Each state is added by registering events in the actual script on the object. Here I will "throw a brick" for everyone, interested friends can study it by themselves.

Now write the code in the different states in order.

FireBallSkill和FirePillarSkill

These two states are brought together because their main logic is similar. In FireBall, the Boss will continuously spray fireballs A, and these fireballs A will fly forward in a fan-shaped arrangement of shotguns, and will cause damage to the player after hitting the player. FirePillar is a skill added by the Boss after half blood. It will summon the fireball B that falls from the sky. The position of the fireball B is random. When it hits the ground or the player, a pillar of fire will be generated to cause damage to the player.

Then prepare the prefabs of the two fireballs first. Do the required animation and hang the required components. Then write a script for both of them to control the logic of flying and attacking players after they are generated. The code below is the Fireball A code.

public class FireBall : MonoBehaviour
{     GameObject Boss;     Animator animator;     Collider2D collider2d;     Vector3 dir;//Initial direction     public float Speed;//Speed     ​​public float Damge;//Damage     public float LifeTime;//Existence time






    void Start()
    {
        Boss = GameObject.Find("Boss");
        animator = GetComponent<Animator>();
        collider2d = GetComponent<Collider2D>();
        dir = transform.localScale;
        Speed = 5;
        Damge = 8f;
        LifeTime = 7f;
    }

    void Update()
    {
        Move();
        LifeTime -= Time.deltaTime;
        if (LifeTime <= 0 || Boss.GetComponent<FireCentipede>().isDead)
        {
            Destroy(gameObject);
        }
    }
    public void Destroy()
    {
        Destroy(gameObject);
    }
    public void Move()//飞行
    {
        if (Boss.transform.localScale.x < 0)
        {
            transform.localScale = new Vector3(dir.x, dir.y, dir.z);
            transform.position += Speed * -transform.right * Time.deltaTime;
        }
        else if (Boss.transform.localScale.x > 0)
        {
            transform.localScale = new Vector3(-dir.x, dir.y, dir.z);
            transform.position += Speed * transform.right * Time.deltaTime;
        }
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))//对玩家造成伤害
        {
            animator.Play("Hit");
            collider2d.enabled = false;
            if (transform.position.x < collision.transform.position.x)
            {
                collision.GetComponent<PlayerCharacter>().BeHit(Vector2.right,Damge);
            }
            else if (transform.position.x >= collision.transform.position.x)
            {                 collision.GetComponent<PlayerCharacter>().BeHit(Vector2.left,Damge);             }         }         else if (collision.CompareTag("Ground"))//explode on ground         {             animator.Play("Hit");             collider2d .enabled = false;         }     } } Everyone will find that the Destroy method is very strange, there is only one sentence, and there is no place to call it in the script. Its real function is as an animation frame event, which is called when the animation plays to the specified frame. I will introduce how to use the animation frame event in a while, let's continue here.










After the prefabs are ready, we write the method of generating them in the Boss script.

Fireball A wants to achieve the effect of shotgun, the main idea is to change the Rotation of each fireball in turn according to a certain angle difference. The generation method is as follows:

for (int i = -5; i < 2; i++)
  {        GameObject fireball = Instantiate(Fireball, null);        Vector3 dir = Quaternion.Euler(0, i * 15, 0) * -transform.right;        fireball.transform. position = Mouth.position + dir * 1.0f;        fireball.transform.rotation = Quaternion.Euler(0, 0, i * 15);    } Fireball B is randomly generated in the air and then falls down. We set its x-axis value to a random value and that's it. The generation method is as follows:





for (int i = 0; i < 5; i++)
  {        int r = Random.Range(-14, 14);//The range here is the length of the platform        GameObject firepillar = Instantiate(FirePillar, null);        firepillar.transform. position = new Vector3(r, 6, 0);   } Then set these two methods as animation frame events, the operation is as follows:




Double-click the corresponding animation to pop up the Animation window, then click the red circle to generate an event, and drag it to the frame where the event needs to be triggered. 

 Then click that pointer to pop up the AnimationEvent interface, select the corresponding script in the Object inside, add the name of the corresponding function in Function, and it’s done. 

PS: There are a few points to note when using animation frame events: 1. Only one of the three parameters can be passed, or there is no parameter. 2. The source of the Function event function must be the Animator component. 3. If Object is empty, it will select the script bound to itself as an event function by default.

After completing the binding of the animation frame event, we can automatically generate a fireball as long as we play the animation. So in the FireBallSkill method and the FirePillarSkill method, we only need to be responsible for playing animations and switching states according to the conditions and processes in the flow chart, and everything is OK. code show as below:

public void FireBallSkill()
    {         C_ani.Play("Attack");         FirePillarCd -= Time.deltaTime;//The CD of the fire pillar skill         if (FireBallAttackTime <= 0 && !isDead)//The skill is released without death         {             state = BossState .Idle;         }         else if (isDead)         {             state = BossState.Death;         }     } public void FirePillarSkill()     {         C_ani.Play("OtherAttack");         background.isChange = true;//Background turns black         Pillar1.GetComponent<Collider2D> ().enabled = false;//Turn off the platform in the sky         Pillar2.GetComponent<Collider2D>().enabled = false;//Turn off the platform in the sky

















        FirePillarCd = 20f;//Reset CD
        if (FirePillarAttackTime <= 0 && !isDead)
        {             state = BossState.Idle;         }         else if (isDead)         {             state = BossState.Death;         }     } In this way, we complete FireBall state and FirePillar The state of writing is up.







DashSkill

In the Dash state, the design here is to let the Boss rush from one side to the other, causing damage to the player if it touches the player during the process. So all we need to do is make the Boss move. Rigidbody2D is directly used here to achieve movement. Of course, the conditions for switching the state should also be well written. Here, the state is switched after reaching the specified point. This part will not elaborate further.

Idle,BeHit,Death

The main function of the Idle state is to give the player a period of time to attack the Boss - the target time is up. In addition, based on the design of this article, the skills of the Boss will be strengthened after half blood, and these changes are also carried out in the Idle state. In the BeHit state, when the player attacks the Boss, the injured animation plays and the blood volume decreases. The Death state is to play the death animation and delete the Boss entity when the Boss HP is less than or equal to zero. code show as below:

public void IdleProccess()
    {         C_ani.Play("Idle");         background.isBack = true;//The background changes back to the initial state         Pillar1.GetComponent<Collider2D>().enabled = true;//The platform opens         Pillar2.GetComponent<Collider2D >().enabled = true;//Enable         FirePillarCd on the platform -= Time.deltaTime;//Fire Pillar Skill CD         if (Hp <= MaxHp / 2 && IdleTime > 0)//Increase the release times of Fire Ball/Fire Pillar after half blood         {             FireBallAttackTime = 5;             FirePillarAttackTime = 7;             IdleTime -= Time.deltaTime;         }         else if (Hp > MaxHp / 2 && IdleTime > 0)         {             FireBallAttackTime = 3;             FirePillarAttackTime = 7;















            IdleTime -= Time.deltaTime;
        }
        if (IdleTime <= 0 && !isHit && !isDead)
        {
            if (FirePillarCd <= 0 && Hp <= MaxHp / 2)
            {
                state = BossState.FirePillar;
            }
            else
            {
                state = BossState.Dash;
            }
        }
        else if (isHit && !isDead)
        {
            state = BossState.BeHit;
        }
        else if (isDead)
        {
            state = BossState.Death;
        }
    }

    public void BeHitProccess()
    {         C_ani.Play("BeHit");//Play the hit animation         IdleTime -= Time.deltaTime;         if (!isHit && !isDead)         {             state = BossState.Idle;         }         else if (isDead)         {             state = BossState.Death;         }     }     public void BeHit(float Damge)//The method called by the player HitBox attacking the Boss     {         Hp -= Damge;         isHit = true;         HitAudio.Play();//Audio playback         EffectAni.Play( "1");//Play the strike effect     } Okay, the main logic of the Boss is finished.


















Set up some skills CD, Boss HP, skill damage and other data, and then simply create a player-controlled protagonist, build a scene, and hang Cinemachine to realize camera follow and range limitation.

 

The project address is as follows:

https://pan.baidu.com/s/1czDhXjecYDR1hzbEwtbpYg

Extraction code: im9u 

Guess you like

Origin blog.csdn.net/m0_69824302/article/details/127819236