tutorial


March 2020 Devlog Post

So, like a lot of people in the US (and world), I’m currently in a self-isolation situation. I’ve always wanted to try working from home for an extended period of time, but last week wasn’t quite what I expected. The main reason being that when working from home in normal circumstances, I can leave whenever I want to go to the store, get lunch, coffee, jog, etc., without fear of contracting a highly contagious and deadly virus. For now, I’ll be in my home office working at my day job as well as my indie endeavors.

My desk, which I’ll probably reorganize in a bit, since I’ll be at it for awhile.

With the extra time at home, I’m hoping to get more work done on some of my indie endeavors. Admittedly, due to a lot of anxiety from the situation, I wasn’t as motivated as I usually am, but I’m hoping now that things have settled a bit, I’ll be able to focus more. That being said, here is the current state of my projects.

MerFight Version 0.8.0

I’m making decent progress on MerFight. I have 8 of the 12 characters playable. The latest characters are Naeco who uses poisonous claws to infect enemies, and Enjellique, who can control different a magical jellements. The game is playable in open alpha through itch.io and Game Jolt. I’m always looking for feedback, especially on the gameplay, so feel free to try it out.

Enjellique, a master of the jellemental arts.
Naeco attacks has a extendable claw used to attack and sometimes poison opponents.

Patreon

My Patreon is still active. I’ve been trying to upload new builds of prototypes there. I’ve yet to, but I also want to post tutorials, particularly focusing in the rollback netcode implementation I’m using for Unity.

That being said, I’m having second thoughts about it. Though I really appreciate my patreons motivating support, I don’t have many, so sometimes I want to show a prototype off or have more people try it, but I also feel that isn’t fair to my Patreons. Additionally, given the current situation, I feel like the money going towards my Patreon could be used for more important things.

So, I’m on the fence. One idea is to pause it. Another idea is to cancel it entirely. A third idea is to make it free temporarily. I’m not sure. Regardless, I’m still thinking on it and planning what to do when April arrives.

Other Projects

Besides my Patreon, I’m working on a few other projects.

I started doing some research for a 2.5D fighting game involving swapping parts as well as fighting on a cylinder. It’s inspired by a 90’s toy called Socket Poppers. I even bought some off Ebay to study them — though admittedly the study wasn’t super fruitful.

Originally, I wasn’t going to make it a tag game, but now I’m having second thoughts.

I also a prototype, one that I want to be part of the rollback tutorial I started. It’s a game where you try and enter various fighting game inputs as quickly as possible.

This is an early version, but the idea is essentially the same.

But going back to what I said about my Patreon, a part of me does want to finish and release it, maybe even for free just to get more eyes on it, but again, that wouldn’t really be fair to my Patreons.

For the rest of March and April, I have some things to think over. Of course, staying in and trying to be healthy and not catch or spread COVID-19 is my current, top priority. Stay safe and well and inside (if you can), everyone.


Dev Log: Jan. 2020

It’s 2020!

So, it’s 2020.  Unfortunately, this year is off to a semi-rough start for me.  It started with anxiety and sickness, and now, I’m having some dental woes.  Just 2 cavities, but I’ve had cavities turn into a lot more; I’ll be fine regardless, but it’s never fun.  That being said, I wanted to write about what I plan on doing in 2020.

2019 Summary

2019 was pretty much the year of MerFight.  I really hit a stride with the game, creating every character model, implementing 5 characters, and showing the game off in public.  I still have a long way to go though.

Despite my progress, I did, however, experience some burnout in 2019.  For 2020, I made a schedule I want to try and keep up with to get decent work on the game done every week but also use as a reminder to take breaks and limit how much I work to prevent a similar setback.

Burnout is not fun…

2020 Plans

Anyway, here is a rather lofty list of things I’d like to accomplish or start in 2020.  I will NOT get all of these done; heck, I may not even touch some, but I like having a lofty list.

Continue MerFight

There are 7 characters left to finish for MerFight.  Plus a ton of other features, and polish, so the chances of finishing it in 2020 are near impossible, even if I worked intensely on it everyday.  Showing the game off at a convention like Combobreaker would be cool; however, I think in trying to do so, I would burn myself out, so I’m going to, instead, focus on trying to get it in a good state for 2021.

New Game Prototypes

I have some new games I’d like to start prototyping and get to stable enough states that I can pause them.  Most are fighting games — shocker. I started doing an interactive roadmap, but that began feeling cumbersome to update and share, but here is a current list:

Swapping Limb, Uh Cylinder Fighter

A fighting game that takes place on a cylinder or a round arena.  Goal is to create a 2D fighting game without corners. I also want to allow players to create unique characters by swapping limbs, inspired by a line of toys I had growing up in the 90’s called Socket Poppers.

I’m also playing around with frame rate; the game is at 60 FPS but the animations are at 24 FPS.
A very strange toy…

Fighter RPG

I feel there have been fighting games with RPG mechanics, but not one like I’m looking for yet. I experimented with this last year during a jam week, and this may be my fallback game if I find my rollback netcode results are abysmal.

CupKick

CupKick is a 3D fighter I’ve been wanting to do for a long time.  It has 3 unique aspects. It only has two buttons (punch and kick), it has no visible UI (clothing destruction is used to display damage), and all characters are based on different desserts.  Unfortunately, the more I’ve explored the design of this, the more I find that doing all 3 things might be difficult. I’ll continue to explore and contemplate this idea though. Maybe it’ll end up being two different games.

Battle High 2 A+ Update

I’d love to do a Battle High 2 update.  There is still one unfinished character, but I’d also love to see if I can integrate my rollback netcode to the game.  I think I need to release or get more comfortable with said netcode in Merfight or another release first though.

Nadine, an unfinished character.

Dimension Swap

This is lofty, and I don’t even see myself starting this till 2021, but I wanted to do a game where it starts as a 3D fighter, but you can switch to a 2D fighter.  So imagine Bloody Roar, but when in animal form, the gameplay changes entirely for both players. Could it be a disaster? Sure, but I’d like to at least prototype it.

Other Goals

Though many are game development related, there are some other goals I have for 2020.

Rollback Netcode Tutorial

I’m currently working on this, but I’d like to make a tutorial for my rollback netcode approach in Unity and share it.  I may start this with just my patrons though, but we’ll see.

Discord

I want to start a Mattrified Games discord.  I’m hesitant because I know I won’t be able to be on it all the time, and I know there are some issues with hosting a server, but it’s something I’d like to try and have a hub for people who like my work to interact with me and each other.

Rigging Script

In 2019, I did a few gigs using my biped constraint script in 3ds max.  I’d like to continue some work with this. I’ve contemplating cleaning and releasing the script or maybe making it a fiverr gig.  I’m not 100% sure, but having a way to make a few extra bucks with the script from time to time would be nice.

Attend Combobreaker (or some other gaming con)

I’ve been to EVO and Magfest, and both were fun to a degree.  I felt a bit out of place at EVO, and I was showing a game off at Magfest so I didn’t really get to enjoy it entirely.  That being said, I hear good things about Combobreaker and would like to attend this year, but NOT while showing a game.  Though showing MerFight at Combobreaker would be cool — if accepted — trying to get something done by May would be a hassle and probably cause burnout.  I’d rather attend the convention first, see how it is, and then think about showing the game off there.

Overall…

This is a lot of stuff.  Additionally, I also have the usual resolutions like being kinder and healthier.  But from this list, I’d be happy if I could accomplish just a few items. So, though 2020 is off to a rough, uncertain start, I’m hoping the schedule I developed can help make it more productive and positive than 2019.


Tutorial: Setting up a 3D Fighting Game Camera Using Cinemachine in Unity3D

This is a simple tutorial exploring the use of a Cinemachine camera in Unity3D for a 3D fighting games such as Virtua Fighter or Tekken.  This is by no means the only solution to achieve this; this tutorial just explores methods that I’ve had some success with when prototyping.

Why use a Cinemachine camera? One nice advantage to a Cinemachine camera is that you can blend into other camera views quickly and easily, so, for example, if you have a unique camera animation for a throw or super move intro, you can easily blend to this animation and back to the main camera using Cinemachine by just simply switching the priority of the virtual camera.

This being said, my Cinemachine camera has the following goals:

  • Track the two characters in the environment
  • Rotate as the two characters move around the scene in any direction.
  • Move in and out as the characters get closer and farther apart.

My scene initially looks like this.  I have two characters, a pink fighter and a teal fighter, which will be tracked by my Cinemachine camera setup.

Initial scene setup

Tracking the Characters

To track the characters, I first create a Cinemachine Target Group Camera (Cinemachine -> Create Target Group Camera).  This creates my Cinemachine Virtual Camera as well as a Cinemachine Target Group.

The Cinemachine Target Group allows me to track multiple transforms.  The following is the Cinemachine Target Group Component setup:

The Cinemachine Target Group Setup

I want the position mode to be Group Center and the Rotation Mode to be Manual.  A script will be applied that’ll rotate this later. I use Late Update for the Update Method.

I set both fighter transforms in the target list with the same weight.  I set the radius to about 1.7. Originally, I was experimenting with the Framing Transposer, where this value is very important; however, for now, just making sure this radius is the same for both transforms is the most important.

The Cinemachine Virtual Camera is setup as follows:

Cinemachine Virtual Camera Setup

The camera is setup to follow and look at the target group.  For the body of the virtual camera, I’m using a Transposer. Again, I originally tried a Framing Transposer, however, I found it was really jittery when rotating the camera.  I later read that the Framing Transposer is better for 2D camera usage than 3D, rotated camera use, so I went back to the basic Transposer.

Anyway, the values are pretty similar to the default values, except I lowered the damping to 0.5 per axis and set the Follow Offset to [0, 2, -4.3333333].  This makes it so the camera is 2 units up and -4.33333333 units away from the center of the target group during runtime.

For the Aim, I use “Same as Follow Target” meaning it’ll use the same rotation as the target group’s transform.

Using this initial setup, the camera should appear like this:

The camera setup just following the center of the targets.

Right now, the camera does a pretty decent job tracking the center of the two characters; however, it doesn’t move back to fit them in view when the pink fighter gets a certain distance away and the camera doesn’t rotate as the pink fighter walks around their opponent.

The next section of this tutorial will go over setting up the camera so it both rotates and moves to track the fighters better.

Rotating and Aligning the Camera

To achieve this, instead of fighting with built-in Cinemachine tools, I decided to write a script.  The MonoBehaviour, Align3DCam, is attached to the Target Group GameObject and appears as follows:

Cinemachine Target Group with Align 3D Cam

TA and TB are the two transforms that will be tracked.  In this case, our fighters.

We then reference the virtual camera itself.  Its Transposer Component will be referenced as well, but this reference will be set during Awake.

Framing Normal is the normal vector.  This is set on Awake based on the follow offset of the virtual camera’s Transposer value.

Distance shows the distance between the two tracked transforms; it is serialized in the inspector for debugging purposes.

Transposer Linear Slope and Transposer Linear Offset are two values that represent a simple linear equation (y = mx + b) where x is the distance between the two tracked transforms and y is the distance along the Framing Normal that the virtual camera will be offset.

The framing helpers are used to help create the slope and offset as well as set the minimum allowed distance so that the camera doesn’t move in too closely when the fighters are standing next to one another.

Now, the following is the script used for Align3DCam:

using Cinemachine;
using UnityEngine;

public class Align3DCam : MonoBehaviour
{
    [Tooltip("The transforms the camera attempts to align to.")]
    public Transform tA, tB;

    [Tooltip("The cinemachine camera that will be updated.")]
    public Cinemachine.CinemachineVirtualCamera virtualCamera;

    /// <summary>
    /// The Transposer component of the cinemachine camera.
    /// </summary>
    private Cinemachine.CinemachineTransposer tranposer;

    /// <summary>
    /// Boolean that is set based on whether or not a virtual camera is supplied.
    /// </summary>
    private bool hasVirtualCamera;

    [SerializeField(), Tooltip("The starting normal of the cinemachine transposer.")]
    private Vector3 framingNormal;

    [SerializeField(), Tooltip("The current distance between the two tracked transforms.")]
    float distance;

    [Tooltip("Slope Value (m) of the linear equation used to determine how far the camera should be based on the distance of the tracked transforms.")]
    public float transposerLinearSlope;

    [Tooltip("Offset Value (b) of the linear equation used to determine how far the camera should be based on the distance of the tracked transforms.")]
    public float transposerLinearOffset;

    [Header("Framing helpers")]
    [Tooltip("The minimum distance allowed between the two transforms before the camera stops moving in and out.")]
    public float minDistance;

    [Tooltip("The minimum distance the camera will be from the tracked transforms.")]
    public float minCamDist;

    [Tooltip("A secondary distance between the two transforms used for reference.")]
    public float secondaryDistance;

    [Tooltip("A secondary distance the camera should be at when the tracked transforms are at the secondary distance.")]
    public float secondaryCamDistance;

    /// <summary>
    /// Function to help determine the
    /// </summary>
    [ContextMenu("Calculate Slope")]
    void CalculateSlopes()
    {
        if (virtualCamera == null)
            return;
        tranposer = virtualCamera.GetCinemachineComponent<CinemachineTransposer>();
        if (transposer == null)
            return;

        // If the application is playing, we don't update the minimum values.
        if (!Application.isPlaying)
        {
            // We get the distance between the transforms currently
            minDistance = Vector3.Distance(tA.position, tB.position);
            distance = minDistance;

            // We get the magnitude of the follow offset vector.
            minCamDist = tranposer.m_FollowOffset.magnitude;
        }

        // We calculate the slope ((y2-y1)/(x2-x1))
        transposerLinearSlope = (secondaryCamDistance - minCamDist) / (secondaryDistance - minDistance);

        // We calculate the offset b = y - mx;
        transposerLinearOffset = minCamDist - (transposerLinearSlope * minDistance);
    }

    private void Awake()
    {
        // Determines if a virtual camera is present and active.
        hasVirtualCamera = virtualCamera != null;
        if (hasVirtualCamera)
        {
            transposer = virtualCamera.GetCinemachineComponent<CinemachineTransposer>();

            if (transposer == null)
            {
                hasVirtualCamera = false;
            }
            else
            {
                // Sets the framing normal by the transposer's initial offset.
                framingNormal = tranposer.m_FollowOffset;
                framingNormal.Normalize();
            }
        }
    }

    // Update is called once per frame
    void LateUpdate()
    {
        // Gets the distance between the two tracked transforms.
        Vector3 diff = tA.position - tB.position;
        distance = diff.magnitude;

        // The Y is removed and the vector is normalized.
        diff.y = 0f;
        diff.Normalize();

        // Adjusts the follow offset of the transposer based on the distance between the two tracked transforms, using a minimum value.
        if (hasVirtualCamera)
        {
            tranposer.m_FollowOffset = framingNormal * (Mathf.Max(minDistance, distance) *
                transposerLinearSlope + transposerLinearOffset);
        }

        // If the two transforms are at the same position, we don't do any updating.
        if (Mathf.Approximately(0f, diff.sqrMagnitude))
            return;

        // We create a quaternion that looks in the initial direction and rotate it 90 degrees
        Quaternion q = Quaternion.LookRotation(diff, Vector3.up) * Quaternion.Euler(0, 90, 0);

        // We create a second one that is rotated 180 degrees.
        Quaternion qA = q * Quaternion.Euler(0, 180, 0);

        // We determine the angle between the current rotation and the two previously created rotations.
        float angle = Quaternion.Angle(q, transform.rotation);
        float angleA = Quaternion.Angle(qA, transform.rotation);

        // The transform's rotation is set to whichever one is closer to the current rotation.
        if (angle < angleA)
            transform.rotation = q;
        else
            transform.rotation = qA;
    }
}

The script is rather lengthy, so I’ll summarize it a bit.  It’s essentially doing two things. Offsetting the camera based on the linear equation values and rotating the camera based on the vector between the two tracked transforms.

The slope values are calculated in the CalculateSlope method, which has a ContextMenu attribute, meaning it can be accessed through the right-click menu of the Align3DCam Component.

The Calculate Slope Context Menu Method

What this does is it takes the current distance of the fighters and the magnitude of the cinemachine camera follow offset position to set the minimum distance and minimum camera distance.  The secondary values are then used to calculate Transposer Linear Slope and Transposer Linear Offset.

Now, to get good secondary values, you’ll have to manually adjust them until you have something you like.  If you use Calculate Slope while the game is playing, the minimum values will not be adjusted, so you can test different secondary values out, and then copy the Component and paste its values when you are no longer running.  I could have probably written a more advanced algorithm that uses a bounding box, but for now, I found this got the job done pretty quickly.

When it comes to rotation, the method works by taking the vector between the two transforms, which is found by subtracting the position of TB from TA.  The y value of this vector is then set to 0 and the vector is normalized.  

A quaternion is then created using Quaternion.LookAt, which takes the normalized diff Vector and Vector3.Up to create a rotation that is essentially look in the direction of this vector.  This quaternion is then multiplied by a 90 degree rotation, thus creating a rotation that will look at both characters; however, this assumes that the TA will always be on the left and TB will be on the right.  If they switch sides, such as one fighter jumping over the other, the camera will rotate and snap quickly like this:

How the camera will look if we only care about one Vector

We certainly don’t want that, so we create another quaternion, which is the first quaternion we created and rotate it 180 degrees on the Y axis, essentially, the same rotation looking in the opposite direction.  We then get the angle between both of these quaternions and the camera’s current rotation. Whichever angle is lower, that is the rotation we use. So now, when jumping over the opponent, the camera will no longer pop to keep the pink player on the left side:

The camera no longer forcing the pink player to the left side.

So, once applied with proper values, the cinemachine virtual camera and target group should work as follows:

The Final Result of the 3D Fighter Cinemachine Camera Setup

As the pink player moves around the teal player, the camera rotates.  The camera moves back as the pink player gets farther away and the camera doesn’t snap sharply when jumping over the opponent.  All of the initial goals have been achieved.

Overall, this is a very simple setup, but it’s a good place for setting up a 3D camera for a fighting game using Cinemachine, especially for early prototypes.  Future features that would probably need to be added are collision with objects in the world if scenes are more complex or using a bounding box to frame fighters in more properly, but again, this is a simple approach to get something off the ground.

If you have questions feel free to comment here or send me a tweet to @mattrified. Additionally, a sample can be found on my Patreon.