1 (edited by MrPonton 2017-02-25 17:16:19)

Topic: Adding/Removing Additional Hit Boxes in a Move

I referenced this in another thread of mine, and I guess I'm just in the giving mood so I'll post a tutorial on how to set up the ability to add and remove hit boxes during a move.

So what's the purpose of this modification? Well, simply put again, the hit boxes setup that UFE does by default is counter-intuitive to a 2D fighter. Hit boxes in UFE by default are set up in a 3D fighter mentality: Linking hit boxes to bones and judging those boxes based off the animations. 2D fighters don't tend to do it this way. When you perform moves in say Street Fighter it will add or remove hit and hurt boxes separately from the animation bone data itself. Also, side note, UFE has inverted terminology, it refers to hit boxes as hurt boxes and hurt boxes as hit boxes. For the sake of simplicity and consistency, I'll be maintaining UFE's terminology over the proper traditional terminology.

This modification will make it so you can add any hit box collider, that normally is only accessible when editing your Character, into the Move Editor while storing said data into the move itself to function when performing the move in game.

For example, here you will see Ken's idle hit boxes (top) and his Jab hit boxes (bottom) in Street Fighter V:
https://snag.gy/DIAOq4.jpghttps://snag.gy/im0VSh.jpg

You'll notice that the hit boxes differ in size from his idle (specifically his head hit box), and there's also two additional hit boxes, one placed in front of him chest high, and another layered behind his hurt box.

You'll be touching many files in this tutorial: ControlsScript.cs, HitBoxesScript.cs, MoveEditorWindow.cs, MoveInfo.cs

MoveInfo.cs

Begin by creating a new class using the Hit class as a reference. I named mine MoveHit (because this class will be used for hit boxes in a move). It will require the properties of an int for what the beginning of the hit interval will be, an int for what the end interval will be, a toggle for the editor to display the hit,  an array of hit boxes to hold, and a clone method:

Locate the following code:

    [System.Serializable]
public class FrameLink: ICloneable {

and paste the following code on a new line above it:

[System.Serializable]
public class MoveHit : ICloneable {
    public int activeFramesBegin;
    public int activeFramesEnds;
    public bool moveHitBoxesToggle;
    
    public HitBox[] hitBoxes = new HitBox[0];
    public object Clone() {
        return CloneObject.Clone(this);
    }
}

Additionally, we'll want to create an array of these in each move. So in the MoveInfo class add a MoveHit[] array property to it defaulting to an array of 0 elements. I called mine moveHits:

Locate the following code:

public Hit[] hits = new Hit[0];

and paste the following line after it:

public MoveHit[] moveHits = new MoveHit[0];

There, we'll use this class when editing a move. So let's jump to the Move Editor so we can begin the path to setting these new boxes.

MoveEditorWindow.cs

Begin by declaring a new private toggle at the top of the document for opening/closing the MoveHits. I named mine moveHitsToggle:

Locate the following line of code:

private bool hitsToggle;

and paste the following line after it:

    private bool moveHitsToggle;

Now let's go create that toggle and list of MoveHits. Put this code after the Hits toggle's code and before the Blockable Area Toggle code. It's basically going to be set up the similarly to the hits toggle:

Locate the following line of code:

// Blockable Area Toggle

and paste this above it.

                        // Hits for Move Hit Boxes Toggle
                        moveHitsToggle = EditorGUILayout.Foldout(moveHitsToggle, "Move Hits (" + moveInfo.moveHits.Length + ")", EditorStyles.foldout);
                        if (moveHitsToggle) {
                            EditorGUILayout.BeginVertical(subGroupStyle); {
                                EditorGUI.indentLevel += 1;
                                List<Vector3> castingValues = new List<Vector3>();
                                for (int i = 0; i < moveInfo.moveHits.Length; i++) {
                                    EditorGUILayout.Space();
                                    EditorGUILayout.BeginVertical(arrayElementStyle);
                                    {
                                        EditorGUILayout.Space();
                                        EditorGUILayout.BeginHorizontal();
                                        {
                                            StyledMinMaxSlider("Active Frames", ref moveInfo.moveHits[i].activeFramesBegin, ref moveInfo.moveHits[i].activeFramesEnds, 1, moveInfo.totalFrames - 1, EditorGUI.indentLevel);
                                            if (GUILayout.Button("", "PaneOptions")) {
                                                PaneOptions<MoveHit>(moveInfo.moveHits, moveInfo.moveHits[i], delegate (MoveHit[] newElement) { moveInfo.moveHits = newElement; });
                                            }
                                        }
                                        EditorGUILayout.EndHorizontal();
                                        EditorGUIUtility.labelWidth = 180;
                                        EditorGUILayout.Space();

                                        // Hurt Boxes Toggle
                                        int amount = moveInfo.moveHits[i].hitBoxes != null ? moveInfo.moveHits[i].hitBoxes.Length : 0;
                                        moveInfo.moveHits[i].moveHitBoxesToggle = EditorGUILayout.Foldout(moveInfo.moveHits[i].moveHitBoxesToggle, "Hit Boxes (" + amount + ")", EditorStyles.foldout);
                                        if (moveInfo.moveHits[i].moveHitBoxesToggle) {
                                            EditorGUILayout.BeginVertical(subGroupStyle);
                                            {
                                                EditorGUI.indentLevel += 1;
                                                if (amount > 0) {
                                                    for (int y = 0; y < moveInfo.moveHits[i].hitBoxes.Length; y++) {
                                                        EditorGUILayout.BeginVertical(subArrayElementStyle);
                                                        {
                                                            EditorGUILayout.BeginHorizontal();
                                                            {
                                                                moveInfo.moveHits[i].hitBoxes[y].bodyPart = (BodyPart)EditorGUILayout.EnumPopup("Body Part:", moveInfo.moveHits[i].hitBoxes[y].bodyPart, enumStyle);
                                                                if (GUILayout.Button("", "PaneOptions")) {
                                                                    PaneOptions<HitBox>(moveInfo.moveHits[i].hitBoxes, moveInfo.moveHits[i].hitBoxes[y], delegate (HitBox[] newElement) { moveInfo.moveHits[i].hitBoxes = newElement; });
                                                                }
                                                            }
                                                            EditorGUILayout.EndHorizontal();
                                                            moveInfo.moveHits[i].hitBoxes[y].collisionType = (CollisionType)EditorGUILayout.EnumPopup("Collision Type:", moveInfo.moveHits[i].hitBoxes[y].collisionType, enumStyle);
                                                            moveInfo.moveHits[i].hitBoxes[y].shape = (HitBoxShape)EditorGUILayout.EnumPopup("Shape:", moveInfo.moveHits[i].hitBoxes[y].shape, enumStyle);
                                                            if (moveInfo.moveHits[i].hitBoxes[y].shape == HitBoxShape.circle) {
                                                                moveInfo.moveHits[i].hitBoxes[y].radius = EditorGUILayout.Slider("Radius:", moveInfo.moveHits[i].hitBoxes[y].radius, .1f, 10);
                                                                moveInfo.moveHits[i].hitBoxes[y].offSet = EditorGUILayout.Vector2Field("Off Set:", moveInfo.moveHits[i].hitBoxes[y].offSet);
                                                            } else {
                                                                moveInfo.moveHits[i].hitBoxes[y].rect = EditorGUILayout.RectField("Rectangle:", moveInfo.moveHits[i].hitBoxes[y].rect);

                                                                EditorGUIUtility.labelWidth = 200;
                                                                bool tmpFollowXBounds = moveInfo.moveHits[i].hitBoxes[y].followXBounds;
                                                                bool tmpFollowYBounds = moveInfo.moveHits[i].hitBoxes[y].followYBounds;

                                                                moveInfo.moveHits[i].hitBoxes[y].followXBounds = EditorGUILayout.Toggle("Follow Character Bounds (X)", moveInfo.moveHits[i].hitBoxes[y].followXBounds);
                                                                moveInfo.moveHits[i].hitBoxes[y].followYBounds = EditorGUILayout.Toggle("Follow Character Bounds (Y)", moveInfo.moveHits[i].hitBoxes[y].followYBounds);

                                                                if (tmpFollowXBounds != moveInfo.moveHits[i].hitBoxes[y].followXBounds)
                                                                    moveInfo.moveHits[i].hitBoxes[y].rect.width = moveInfo.moveHits[i].hitBoxes[y].followXBounds ? 0 : 4;
                                                                if (tmpFollowYBounds != moveInfo.moveHits[i].hitBoxes[y].followYBounds)
                                                                    moveInfo.moveHits[i].hitBoxes[y].rect.height = moveInfo.moveHits[i].hitBoxes[y].followYBounds ? 0 : 4;

                                                                EditorGUIUtility.labelWidth = 150;
                                                            }

                                                            EditorGUILayout.Space();
                                                        }
                                                        EditorGUILayout.EndVertical();
                                                    }
                                                }
                                                if (StyledButton("New Box"))
                                                    moveInfo.moveHits[i].hitBoxes = AddElement<HitBox>(moveInfo.moveHits[i].hitBoxes, new HitBox());

                                                EditorGUI.indentLevel -= 1;
                                            }
                                            EditorGUILayout.EndVertical();
                                        }
                                    }
                                    EditorGUILayout.EndVertical();
                                }

                                if (StyledButton("New Hitbox"))
                                    moveInfo.moveHits = AddElement<MoveHit>(moveInfo.moveHits, new MoveHit());
                                EditorGUI.indentLevel -= 1;
                            }
                            EditorGUILayout.EndVertical();
                        }

With the current code you are able to add these additional move hits, and within each move hit you are able to add additional hit boxes. You can set up groups of hit boxes to last during certain frames of the move itself now. You should be able to set up your move to be similar to Ken's jab above.

One last thing on this sheet is we want to be able to see the new boxes as we create them. So locate the following section of code:

if (loadHurtBoxes){
                foreach (Hit hit in moveInfo.hits){
                    if (animFrame >= hit.activeFramesBegin && animFrame <= hit.activeFramesEnds) {
                        if (hit.hurtBoxes.Length > 0){
                            hitBoxesScript.hitConfirmType = hit.hitConfirmType;
                            hitBoxesScript.activeHurtBoxes = hit.hurtBoxes;
                        }
                        break;
                    }else{
                        hitBoxesScript.activeHurtBoxes = null;
                    }
                }

and add the following below it:

                List<HitBox> combinedHitBoxes = new List<HitBox>();
                if (moveInfo.moveHits.Length > 0) {
                    foreach (MoveHit hit in moveInfo.moveHits) {
                        if (animFrame >= hit.activeFramesBegin && animFrame <= hit.activeFramesEnds) {
                           if (hit.hitBoxes.Length > 0) {
                                foreach (HitBox hitBox in hit.hitBoxes) {
                                    hitBox.position = hitBoxesScript.GetTransform(hitBox.bodyPart);
                                    hitBox.rendererBounds = hitBoxesScript.GetBounds();
                                    combinedHitBoxes.Add(hitBox);
                                }
                            }
                        }
                    }
                    hitBoxesScript.activeMoveHitBoxes = combinedHitBoxes.ToArray();
                } else {
                    hitBoxesScript.activeMoveHitBoxes = null;
                }

However, currently the game does not recognize these hit boxes though they do exist. So let's begin editing the game code.

HitBoxesScript.cs

Let's begin by adding a HitBox[] array to the HitBoxesScript class to parallel the activeHurtBoxes HurtBox[] array:

Locate the following line of code:

    [HideInInspector] public HurtBox[] activeHurtBoxes;

and add the following on a new line after it:

    [HideInInspector] public HitBox[] activeMoveHitBoxes;

Now that that list is created, let's tell the system to render these hit boxes by adding them to the list of existing character hit boxes to render.

Go to the OnDrawGizmos() method. In there at the top of the method we're going to create a temporary list of hit boxes that is made up of both the character's hit boxes and the move's hit boxes. Then, we'll update the foreach (HitBox hitBox in hitBoxes) to use this new list:

Locate the following lines of code:

        // HITBOXES
        if (hitBoxes == null) return;
        int mirrorAdjust = controlsScript != null? controlsScript.mirror : -1;


        foreach (HitBox hitBox in hitBoxes) {

and replace the code with the following:

        // HITBOXES
        if (hitBoxes == null) return;
        int mirrorAdjust = controlsScript != null? controlsScript.mirror : -1;

        List<HitBox> temp = new List<HitBox>();
        foreach (HitBox hitBox in hitBoxes)
            temp.Add(hitBox);
        if (activeMoveHitBoxes != null) {
            foreach (HitBox hitBox in activeMoveHitBoxes) {
                temp.Add(hitBox);
            }
        }
        HitBox[] combinedListOfHitBoxes = temp.ToArray();

        foreach (HitBox hitBox in combinedListOfHitBoxes) {

Now any Move's hit boxes will render in the scene editor while playing the game. Next we need to update the TestCollision() method to register checking the move's active hit boxes. Go to one of the overloaded TestCollision() methods that has parameters of HurtBox[] hurtBoxes, and HitConfirmType hitConfirmType. Then combine the list of character hit boxes with the list of move hit boxes again before passing in the combined list instead of just the character hit boxes:

Locate the following line of code:

        return HitBoxesScript.TestCollision(this.hitBoxes, hurtBoxes, hitConfirmType, controlsScript.mirror);

and replace with the following:

        List<HitBox> hitBoxesList = new List<HitBox>();
        foreach(HitBox hitbox in this.hitBoxes)
            hitBoxesList.Add(hitbox);
        if (activeMoveHitBoxes != null) {
            foreach (HitBox hitbox in this.activeMoveHitBoxes)
                hitBoxesList.Add(hitbox);
        }
        return HitBoxesScript.TestCollision(hitBoxesList.ToArray(), hurtBoxes, hitConfirmType, controlsScript.mirror);

We now need to do the same for another overload of the TestCollision() method. This time for the one with a single parameter of a HitBox[] opHitBoxes:

Locate the following line:

        float totalPushForce = 0;

and locate the following line shortly below it:

        return totalPushForce;

Delete all code between those two lines and replace with the following code in between both lines (This will also fix the rectangle v rectangle collider bug):

        List<HitBox> temp = new List<HitBox>();
        foreach (HitBox hitBox in hitBoxes)
            temp.Add(hitBox);
        if (activeMoveHitBoxes != null)
            foreach(HitBox hitBox in hitBoxes)
                temp.Add(hitBox);
        HitBox[] combinedHitBoxes = temp.ToArray();
        foreach (HitBox hitBox in combinedHitBoxes) {
            if (hitBox.collisionType != CollisionType.bodyCollider)
                continue;
            foreach (HitBox opHitBox in opHitBoxes) {
                if (opHitBox.collisionType != CollisionType.bodyCollider)
                    continue;
                Vector3 opHitBoxPosition = opHitBox.position.position;
                Vector3 hitBoxPosition = hitBox.position.position;
                if (!UFE.config.detect3D_Hits){
                    opHitBoxPosition = new Vector3(opHitBox.position.position.x, opHitBox.position.position.y, 0);
                    hitBoxPosition = new Vector3(hitBox.position.position.x, hitBox.position.position.y, 0);
                }
                if (hitBox.shape == HitBoxShape.rectangle && opHitBox.shape == HitBoxShape.rectangle) {
                    float mirror = currentMirror ? 1 : -1;
                    float mirrorDiff = mirror > 0 ? opHitBox.rect.width : 0f;
                    Rect opHitBoxRectanglePosition = new Rect(
                                                        (((opHitBox.rect.x + mirrorDiff) * mirror) + opHitBoxPosition.x) - opHitBox.rect.width,
                                                        opHitBox.rect.y + opHitBoxPosition.y,
                                                        opHitBox.rect.width,
                                                        opHitBox.rect.height);
                    mirrorDiff = mirror > 0 ? hitBox.rect.width : 0f;
                    Rect hitBoxRectanglePosition = new Rect(
                                                       ((hitBox.rect.x + mirrorDiff) * -mirror) + hitBoxPosition.x,
                                                       hitBox.rect.y + hitBoxPosition.y,
                                                       hitBox.rect.width,
                                                       hitBox.rect.height);
                    if (opHitBoxRectanglePosition.Overlaps(hitBoxRectanglePosition)) {
                        totalPushForce += 0.2f;
                    }
                } else if (hitBox.shape == HitBoxShape.circle && opHitBox.shape == HitBoxShape.circle) {
                    float dist = Vector3.Distance(opHitBoxPosition, hitBoxPosition);
                    if (dist <= opHitBox.radius + hitBox.radius)
                        totalPushForce += (opHitBox.radius + hitBox.radius) - dist;
                }
            }
        }

Last for the HitBoxesScript is to add the check to mark a move's hit box as being hit. So locate the GetStrokeHit() method and after the foreach checking the hitBoxes, add an a foreach for active Move hit boxes only if the list of active move hit boxes exists:

Locate the following lines:

    public HitBox GetStrokeHitBox(){
        if (!isHit) return null;
        foreach (HitBox hitBox in hitBoxes) {
            if (hitBox.state == 1) return hitBox;
        }

and add the following lines below it:

        if (activeMoveHitBoxes != null)
            foreach (HitBox hitBox in activeMoveHitBoxes)
                if (hitBox.state == 1) return hitBox;

Almost there! Now it's time to fix the other side of the game, the control script. Where we will be setting that list of active hit boxes in a Move and sending it to the Hit Box Script.


ControlsScript.cs

We're going to have to add a check for each active move hit box to see if it's colliding with an opponent's/projectile's box. At the bottom of the ReadMove() method, Create a new list of Hit Boxes dedicated to the active Move Hit Boxes.. Then for each moveHit in the move, check if there are any active move hit boxes and send them to the hit box script as the active move hit boxes.

look for the following line of code:

        if(move.currentFrame >= move.totalFrames) {
            if (move.name == "Intro") {
                introPlayed = true;
                UFE.CastNewRound();
            }
            if (move.armorOptions.hitsTaken > 0) comboHits = 0;
            KillCurrentMove();
        }

and paste the following code below it:

        // Check Move Hits
        List<HitBox> activeMoveHitBoxes = new List<HitBox>();
        foreach (MoveHit moveHit in move.moveHits) {
            if (move.currentFrame >= moveHit.activeFramesBegin &&
                move.currentFrame <= moveHit.activeFramesEnds) {
                if (moveHit.hitBoxes.Length > 0) {
                    foreach (HitBox hitBox in moveHit.hitBoxes) {
                        hitBox.position = myHitBoxesScript.GetTransform(hitBox.bodyPart);
                        hitBox.rendererBounds = myHitBoxesScript.GetBounds();
                        // Needs position to be set
                        if (hitBox != null && hitBox.bodyPart != BodyPart.none && hitBox.position != null) {
                            bool visible = hitBox.defaultVisibility;

                            if (move != null && move.bodyPartVisibilityChanges != null) {
                                foreach (BodyPartVisibilityChange visibilityChange in move.bodyPartVisibilityChanges) {
                                    if (visibilityChange.castingFrame == 0 && visibilityChange.bodyPart == hitBox.bodyPart) {
                                        visible = visibilityChange.visible;
                                        visibilityChange.casted = true;
                                    }
                                }
                            }
                            hitBox.position.gameObject.SetActive(visible);
                        }
                        activeMoveHitBoxes.Add(hitBox);
                    }
                }
            }
            myHitBoxesScript.activeMoveHitBoxes = activeMoveHitBoxes.ToArray();
        }

Now we want to make sure the move hit boxes are cleared when a move ends. So go to the KillCurrentMove() method and after the call to clear the active hurt boxes, clear the active move hit boxes:

Locate the following line:

        myHitBoxesScript.activeHurtBoxes = null;

and add the following line below it:

        myHitBoxesScript.activeMoveHitBoxes = null;

Lastly, we want to make sure that body colliders being checked also include the Move's active body colliders. At the top of the DoFixedUpdate() method you'll find a comment for "// Character colliders based on collision mass and body colliders". There after the if statement we'll put our code to check all possible hit boxes:

Locate the following lines:

        // Character colliders based on collision mass and body colliders
        normalizedDistance = Mathf.Clamp01(Vector3.Distance(opponent.transform.position, transform.position) / UFE.config.cameraOptions.maxDistance);
        if (!ignoreCollisionMass && !opControlsScript.ignoreCollisionMass) {

and add the following lines below it:

            List<HitBox> opCombinedHitBoxes = new List<HitBox>();
            foreach (HitBox hitBox in opHitBoxesScript.hitBoxes)
                if (!hitBox.visibility)
                    opCombinedHitBoxes.Add(hitBox);
            if (opHitBoxesScript.activeMoveHitBoxes != null)
                foreach (HitBox hitBox in opHitBoxesScript.activeMoveHitBoxes)
                    opCombinedHitBoxes.Add(hitBox);

Locate below that

    float pushForce = myHitBoxesScript.TestCollision(opHitBoxesScript.hitBoxes);

and replace with the following:

       float pushForce = myHitBoxesScript.TestCollision(opCombinedHitBoxes.ToArray());

That should be everything. Now you are able to add your own custom hit boxes during specific frames of a move.

Oh, there's one more thing, you might be asking, "But how do I change the character hit boxes like in your example of Ken's head?". This effort requires no code changes (yay). Using the existing invincibility frames function of a move, set the invincibility to whatever frames you want the Character's hit boxes cleared. It will clear all or none of the hit boxes depending on if you have marked that frame to be invincible. Doing that, you can then add a new MoveHit and build up your Character's hit boxes with whatever alterations you want.

https://snag.gy/pikKhZ.jpg

Share

Thumbs up +3 Thumbs down

Re: Adding/Removing Additional Hit Boxes in a Move

Updated tutorial for better clarifying some code changes.

Share

Thumbs up Thumbs down

3 (edited by MrPonton 2017-02-25 17:18:39)

Re: Adding/Removing Additional Hit Boxes in a Move

Fixed some code from the ControlsScript.cs where it had some Rects that I was using for debugging and you probably were not.

Also forgot to add this to the MoveEditorWindow.cs, without this code you weren't able to see your moveHitBoxes while using the move animation preview tool (though they were being stored). Sorry about that:

One last thing on this sheet is we want to be able to see the new boxes as we create them. So locate the following section of code:

if (loadHurtBoxes){
                foreach (Hit hit in moveInfo.hits){
                    if (animFrame >= hit.activeFramesBegin && animFrame <= hit.activeFramesEnds) {
                        if (hit.hurtBoxes.Length > 0){
                            hitBoxesScript.hitConfirmType = hit.hitConfirmType;
                            hitBoxesScript.activeHurtBoxes = hit.hurtBoxes;
                        }
                        break;
                    }else{
                        hitBoxesScript.activeHurtBoxes = null;
                    }
                }

and add the following below it:

                List<HitBox> combinedHitBoxes = new List<HitBox>();
                if (moveInfo.moveHits.Length > 0) {
                    foreach (MoveHit hit in moveInfo.moveHits) {
                        if (animFrame >= hit.activeFramesBegin && animFrame <= hit.activeFramesEnds) {
                           if (hit.hitBoxes.Length > 0) {
                                foreach (HitBox hitBox in hit.hitBoxes) {
                                    hitBox.position = hitBoxesScript.GetTransform(hitBox.bodyPart);
                                    hitBox.rendererBounds = hitBoxesScript.GetBounds();
                                    combinedHitBoxes.Add(hitBox);
                                }
                            }
                        }
                    }
                    hitBoxesScript.activeMoveHitBoxes = combinedHitBoxes.ToArray();
                } else {
                    hitBoxesScript.activeMoveHitBoxes = null;
                }

Share

Thumbs up +2 Thumbs down

Re: Adding/Removing Additional Hit Boxes in a Move

Why isnt this implemented?  will definately makes the engine easire to work with 2d sprites

Share

Thumbs up +1 Thumbs down

Re: Adding/Removing Additional Hit Boxes in a Move

kokakee wrote:

Why isnt this implemented?  will definately makes the engine easire to work with 2d sprites

Thanks for the kind words.

Share

Thumbs up Thumbs down

Re: Adding/Removing Additional Hit Boxes in a Move

It is excellent idea. I support you.

Share

Thumbs up Thumbs down