Langsung ke konten utama

Unggulan

Dynamic First Person Controller Mobile Kit Asset - First Release

Advanced  Drag and Drop Starter Kit for your First Person Mobile game projects Pack includes several other features as starter kit for making your first person mobile game Drag and drop it in your scene, and you have an advanced first person controller that feels dynamic and not boring , so you can focus on other aspect of your mobile game without worrying and spending time making or improving your own first person controller . (Kit prefab includes the controller and the canvas ready-made)  Get Asset: Test Build:  Download test build for android in Itch.io Features included: First Person Camera Touch for mobile Movement Controller with acceleration, land momentum, etc Mobile Button (customizable) Mobile Joystick (customizable) Additional Input for Unity editor Game Sound Effect Built-in Game Settings Supported platforms includes all Android and IOS device Documentation            

Creating RPG hack & slash controller combat system in Unity | Game development journey

    Short story... Growing up as a teenager, I don’t get any chance to play the trending game on the market, one of which is Genshin Impact, and that due to my potato device. I’m clearly desperate about this, since I’m a big fan of JRPG games, both turned-base and hack & slash. Getting into game dev doesn’t resolve my desperation, because still, my i3 gen 7 ram 4 laptop couldn’t handle those complex mechanism.


    It was that time when I discovered this short video. I'm always turn on every time I watch these kind of gameplay. I immediately decided to make one with Unity, and I did, I spent about a month and an half, but the results was crappy and unfinished.
This is how the whole project works by the way

    First of all, I’ll assume you already got the basic, and familiar with Unity. If you're here to learn, note that you don't have to perfectly follow the things I did, I won't cover every part of it, and don't be perfectionist for this time. I created this about 8 months ago, so I might not be able to explain it clearly.

SETTING UP THE BASIC CONTROLLER

Third person camera

    I watched Filmstorm's tutorial for this (I knew him from MixAndJam, and his tutorial are complete) Just watch it and you'll know what to do with the camera. that's all for the camera controller. 

Basic movement

Animation Controller

    For the movement, I look through some of the JRPG game, and they pretty much got walk, jump, and dash, for their main movement. 

    For locomotor movement, I used the standard asset third person controller, then I just straightly retarget the animator. The standard asset's TPC has the solid dynamic movement transition, and most rotation mechanism they did in the script is what I need the most (if you have dealt with quaternion, you'll know what I mean). I also did some changes though, like disabling the crouch, and adjusting the animation controller. For disabling crouch, I believe you could do that yourself. As for adjusting the animator controller, I basically simplify the ground blend tree animation, and change all the animation with the animation I have selected. 

Simplified Animator Controller
Original Standard asset animator controller

    If you're curious how blend tree work, I suggest you watch this video first 2 dimensional blend trees (iHeartGameDev), it's an awesome way to make dynamic animation transition. The key for this blend tree to work smoothly is by using root motion, and disabling the bake pose, explanation incase you need it Root Motion Explained (Roundbeargames) or maybe Should You Use Root Motion?. I'm using this animation package Oriental Sword Animation Asset Package, it was free for a limited time, and I managed to get some. After dealing with these both, you should have a functional locomotor movement. 

    For jump, I want to be able to adjust the jump height, but the Oriental Sword jump animation is not separated. I had to separate each part from the start of jump, when floating in the air, and when landing (Just copy and paste the animation keys from the original jump animation). When jumping, the root motion should be disabled, and the jump should be fully controlled through script. 

Animation when start jumping

Animation when floating

Jump sub-state machine

    If you're wondering why  I have 2 floating animation, I tried to vary the jump by having a spin float and normal float. 
    One of the problem when doing this is that, Oriental Sword jump animation's root is not fixed, and because the root motion is disabled when jumping, the body will jump higher than the actual game object. To fix this, I took from start jump animation root animation key, and replace it on the floating animation. Incase you're having problem to play with the animation transition read this https://docs.unity3d.com/Manual/class-Transition.html

    For dash, this one is quite tricky (To be honest, my dash mechanism is not the best you can find, so feel free to make it yourself). First, I'm using dotween to move things easier, and I need two other things two make this works; I need a distance, also the direction. The direction can simply be transform.forward or if you want to make it even more advanced, you can calculate direction to a certain target. 
    The problem is in the distance, notice on image below, if there's an obstacle in front of the player, and the dash distance is fixed, the player either will pass through the wall, or probably stuck, and have a jittering movement. 
Dash
    So in order to prevent that, I had to detect all the obstacle first by using boxcast (I'm using boxcast because raycast would have a trouble detecting a gap), then calculate a distance from the closest point among of all the obstacle to the player. But this method would make  the player clip into the wall, and even worse stuck inside a wall (see images below). 
Boxcast finding the closest point for dash distance
    To take a step further, I need to calculate a center line that'll make a right triangle with the straight line from player position to boxcast closest point before. this center line length would be an optimal distance for the player to dash.
A new straight line that makes a right triangle
    To calculate the center line length, I need to calculate a the distance from boxcast closest point to a straight line from player transform.forward. This madlad genius Sebastian Lague had already solved the problem, I took his formula, and it works like a charm!
Formula to calculate the distance
    I could use the phytagoras theorem to find the center line length, then It'll be the distance the dash should travel to.

public void Dash(float DashLength, float DashTimer, Vector3 Dir)
{
    float OriginalLength = DashLength;
    float CenterLength = CalculateCenterLength();
    if (DashLength > CenterLength && Cast) 
    {
        DashLength = CenterLength - 0.5f;
        CenterLength = CenterLength < 0f? 0.1f: CenterLength;
    }
    DashTimer *= DashLength/OriginalLength;
    m_DashTimerRef = DashTimer;
    transform.DOMove(transform.position + (Dir * DashLength), DashTimer);         
}

This is an additional utility function to calculate center length easier. The function is a bit confusing, but it works. 

// If you found it confusing, so I am. Don't worry, you can create your own func for this
float PhytagorasFunc(float x, float y, float r){
      if (r == 0) return Mathf.Sqrt(Mathf.Pow(x, 2) + Mathf.Pow(y, 2));
      if (x == 0) return Mathf.Sqrt(Mathf.Pow(r, 2) - Mathf.Pow(y, 2));
      if (y == 0) return Mathf.Sqrt(Mathf.Pow(r, 2) - Mathf.Pow(x, 2));
      return 0;
}

Code for calculating the center length

float CalculateCenterLength()
{
    // I'm from 8 months in the future, and I don't why this code works lol
    Vector3 ForwardLine = transform.position + transform.forward * m_CastMaxDistance;

    Vector3 A = transform.position + transform.forward * 0.5f;
    Vector3 B = ForwardLine + transform.forward * 0.5f;
    Vector3 C = Vector3.Scale(hit.point, new Vector3(1, 0, 1)) + transform.forward * 0.5f; // y is not used, so it's okay

    float AC = PhytagorasFunc((C - A).x, (C - A).z, 0);
    float CB = Mathf.Abs((C.x - A.x) * (-B.z + A.z) + (C.z - A.z) * (B.x - A.x)) / PhytagorasFunc(-B.z + A.z, B.x - A.x, 0);

    return PhytagorasFunc(0, CB, AC);
    
}
Again... This is how I made this whole part
Incase you want to display the boxcast, https://answers.unity.com/questions/1156087/how-can-you-visualize-a-boxcast-boxcheck-etc.html.

Combat system

    Again, read this first https://docs.unity3d.com/Manual/class-Transition.html if you're not familiar with animation transition, we'll be working with transition a lot in here. 

    After observing some JRPG games, I can conclude that mostly combat systems need these things.

  • a chain of combo attack
  • slight dash every attack
  • sphere cast to detect enemy
  • collision to damage enemy

Combo System

    At first, I thought attaching event on the animation, and play the attack using trigger parameter, are the most reasonable things to do, but it doesn't work for me. I tried many approaches to make this work, logically it should works perfectly, but it always end up looks unnatural, and unextendable also spaghettiful. So in the end I use 3D Game Kit as a reference, and I found out that the key is in state time.
Combat sub-state machine

Combat System Paper

    For each animation I set the condition to state time is greater than a certain value (moment when slash is done), and when attack trigger is triggered. using the concept in code below would work, but there's still two last problem to solve in order the combat system to function perfectly.

void Update()
{   
    if (m_Attack)
    {
        // If in transition, there would be 2 animation overlapping
        // State time should be the second animation state time when transition, or else 
        // it'll consider the second animation had been running for a while
        if (!m_Animator.IsInTransition(0)) 
            m_Animator.SetFloat(
            	"State Time", 
            	Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f));
        else m_Animator.SetFloat(
            	"State Time", 
            	Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(1).normalizedTime, 1f));
    
    if (Input.GetMouseButtonDown(0) && m_ThirdPersonCharacter.m_IsGrounded && !m_Controller.m_isDash){
        m_Animator.SetTrigger("Meele Combat"); 
}            

1. A new combo started without trigger after a previous combo ended

    In the 3D Game Kit, the attack trigger is perfectly functional without resetting it, but not for mine. Whenever I trigger the attack trigger, it won't be immediately reset. After the combo attack is transitioned to the basic locomotor animation, it'll start a new combo without me triggering the attack trigger. To fix this, I just simply need to reset it through script.

// This script should be attach to the animation, not game object!
public class CombatRunTimeManager : StateMachineBehaviour
{  
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // When the current animation is transitioning to the basic locomotor animation (grounded)
        if (animator.GetAnimatorTransitionInfo(0).IsName(animator.GetNextAnimatorClipInfo(0)[0].clip.name + " -> Grounded")) animator.ResetTrigger("Meele Combat");
                     
    }
}

2. Attack animation took too long to end before I can move again

   To fix this, I created two separate transition that goes to the same animation which is basic locomotor (grounded). You can see in these pictures below on the right top side there's two transition that led to the same animation, one is the one that has conditions to move and has no exit time, and the other is just a normal transition that has exit time.

With condition but no exit time
No condition but with exit time

if (move != Vector3.zero) m_Animator.SetBool("Skip Close", true);
  else  m_Animator.SetBool("Skip Close", false);

    Using this method, whenever you're not walking after you stop combo, your attack animation will play until the end, but if you try to walk, the animation will transition to the locomotor animation halfway. 

    If you're having a trouble trying to calls a function or run a code if the attack is running, try this code:

void CheckCombat()
{
    if (m_Animator.GetCurrentAnimatorStateInfo(0).IsTag("Combat"))
    {
        // is attacking
    }
    else {
        is not attacking
    }
}

Dash attack

    It would enhance the feeling if the combo attack got a slight dash, so I took the previous Dash function, and calls it every time attack animation is running. What's cool about this code below is that, it is adjustable, so you could have a normal attack that only has a slight dash, and you could also have a dash attack (singleattack06 in the image) seperately.

Combat sub-state machine

// This code is ugly, but it works(again...), try to write it your own if you have a better code! public class CombatRunTimeManager : StateMachineBehaviour { CombatManager m_CombatManager; public float DashSpeed; public float DashTimer; public float DashStart; public bool Dash; override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { m_CombatManager = animator.GetComponent(); Dash = false; } override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { float animationLength = Mathf.Repeat(animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f); if (animationLength > DashStart && !Dash) { m_CombatManager.Dash(DashSpeed, DashTimer, Vector3.Scale(animator.transform.forward, new Vector3(1, 0, 1)).normalized); Dash = true; } } }

Detecting enemies

Collider[] Enemyhit = Physics.OverlapSphere(transform.position, m_EnemyCastDistance, EnemyLayerMask);
if (Enemyhit.Length > 0){
    foreach(Collider hit in Enemyhit) {
        if (shortestEnemyHit == null) shortestEnemyHit = hit;
        float shortestLength = (shortestEnemyHit.transform.position - transform.position).magnitude;
        float currentLength = (hit.transform.position - transform.position).magnitude;
        if (shortestLength > currentLength) shortestEnemyHit = hit;
    }
    m_Target = shortestEnemyHit.transform;
    Debug.DrawLine(transform.position, shortestEnemyHit.transform.position, Color.cyan);
}else {
    shortestEnemyHit = null;
    m_Target = null;
}

Take it or leave it.

Attacking enemies

    By far the simplest part that I made. You should use interface for a tidy code. Clearly there are other better approaches to attack the enemy, but for me, this should work for now.

public class Weapon : MonoBehaviour
{
    void OnTriggerEnter(Collider col)
    {
        if (col.gameObject.tag == "Enemy")
        {
            IDamageable damageable = col.gameObject.GetComponent>();
            damageable.Damage(m_WeaponDamage);
        }
    } 
}

FINISHING

Adding VFX slash 

    I'm obsessed with VFX slash in JRPG game, and I want to make it as well. I've watch several tutorials to make VFX slash, but most of them are not the one I'm looking for. Some of them need Amplify shader editor to make, and I haven't bought it. But fortunately I manage to find one tutorial that is quite easy and actually the one I'm looking for, thanks to this man Hovl Studio VFX slash. I followed his tutorial, and after doing a bit of adjustment, here's the result:

VFX slash
    It's not the best, but it works for now anyway. I use StateMachineBehaviour that attached to each attack, and play the VFX every time it slash. 

Shake effect

    Must have if you want more feeling on your slash. Since I used cinemachine to make the tpc, therefore I could just use cinemachine impulse to shake the camera.

[SerializeField] CinemachineImpulseSource Impulse;
public void Shake()
{
    Impulse.GenerateImpulse();
}

    Since I'm using OnTriggerEnter to attack enemy, I could simply add a few line of code to make the camera shake every time it hits enemy or obstacle.

void OnTriggerEnter(Collider col)
{
    if (col.gameObject.tag == "Obstacle")
    {
        if (m_Animator.GetCurrentAnimatorStateInfo(0).IsTag("Combat") &&
                (Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f) > 0.2f ||
                    Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f) < 0.8f ))
        {
            m_CombatManager.Shake();
        }
    }
    if (col.gameObject.tag == "Enemy")
    {
        if (m_Animator.GetCurrentAnimatorStateInfo(0).IsTag("Combat") &&
                (Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f) > 0.2f ||
                    Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f) < 0.8f ))
        {
            IDamageable damageable = col.gameObject.GetComponent>();
            damageable.Damage(m_WeaponDamage);
            m_CombatManager.Shake();
        }
    }
}


    That's pretty much I could explain for this project. In the end, many of the part I worked on, are from either tutorial or asset, and I believe it's perfectly fine for developer to watch tutorial for most of their time or use asset on their game, as long as it's not considered illegal. 

    Make sure post your comment below if you have any question or just want to discuss something, I'll gladly answer it! 

    adventurer developer youtube, subscribe to it incase you want to follow my development journey, I won't be posting frequently, but your subscribe will means a lot to me!

Thanks for reading.

Komentar

Postingan Populer