3
\$\begingroup\$

Right now I'm finalizing my character raycast controller, but have come to the realization that Unity's tag system, which I'm using as a way for my raycasts to detect different types of platforms, is limited and may cause issues further into development when I start adding more platform types, or use the controller for AI (do I want my AI to be affected the same way the player is affected by platforms?).

So far I've got a bunch of different platforms, like passable platforms, icy blocks and jump boosts. These all work great, and change the character's default movement parameters, but I rely on things like this in order for changes to be made:

if (hit.collider.tag == "platformPassable")

Is there a better way to do this? I am not the most experienced programmer, but my initial solution was to create a Platform class with a TypeOfPlatform enum that will allow me to make sub classes that implement changes based on this type, but I'm worried that having to access a component for each raycast on a platform on which the character or AI is standing will lead to horrible performance issues. Would I have to reference the base class in the character controller, or would I access the character controller in the base class?

I'm just not very comfortable with inheritances like this and would appreciate some guidance.

Update: here's a segment of my code (there are many other likes this) that hopefully helps illustrate what I believe will become problematic as I continue development:

private void MoveVertically(ref Vector3 deltaMovement)
{
    float directionY = Mathf.Sign(deltaMovement.y);
    float rayLength = Mathf.Abs(deltaMovement.y) + skinWidth;

    for (int i = 0; i < verticalRayCount; i++)
    {
        Vector2 rayOrigin = (directionY == -1) ? raycastOrigins.bottomLeft : raycastOrigins.topLeft;
        rayOrigin += Vector2.right * (verticalRaySpacing * i + deltaMovement.x);

        RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, collisionMask);

        Debug.DrawRay(rayOrigin, Vector2.up * directionY * rayLength, Color.red);

        if (hit)
        {
            if (hit.collider.tag == "platformPassable" || hit.collider.tag == "icePassable")
            {
                State.IsCollidingWithPassablePlatform = true;

                if (directionY == 1)
                {
                    State.IsCollidingAbove = false;
                    continue;
                }

                if (CanJumpDown)
                {
                    State.IsCollidingBelow = false;
                    continue;
                }
            }

            if (hit.collider.tag == "iceNormal" || hit.collider.tag == "icePassable")
            {
                Parameters.accelerationTimeGrounded = 1.0f;
                Parameters.accelerationTimeAirborne = 3.0f;
            }

            else if (overrideParameters == null)
            {
                Parameters.accelerationTimeGrounded = defaultParameters.accelerationTimeGrounded;
                Parameters.accelerationTimeAirborne = defaultParameters.accelerationTimeAirborne;
            }

            deltaMovement.y = (hit.distance - skinWidth) * directionY;
            rayLength = hit.distance;

            if (State.IsClimbingSlope)
            {
                deltaMovement.x = deltaMovement.y / Mathf.Tan(State.SlopeAngle * Mathf.Deg2Rad) * Mathf.Sign(deltaMovement.x);
            }

            State.IsCollidingBelow = directionY == -1;
            State.IsCollidingAbove = directionY == 1;
            CanJumpDown = false;
        }
    }

As you can see there are a lot of platform types that are somewhat similar. Plus, since this script will be placed on any character, such as the player or the enemy, both of these entities will be bound to the same movement parameters. What if I didn't want an enemy to slide around on an ice block? I'm not sure there's a way to prevent this in my code in its current structure.

\$\endgroup\$
  • \$\begingroup\$ Can you describe what specific limitations or issues of tags you're trying to solve? This way we can avoid re-creating similar issues in proposed answers. ;) \$\endgroup\$ – DMGregory Jan 23 '18 at 23:49
  • \$\begingroup\$ I guess I'm just concerned that I'm going to run out of tags, especially since I already have 10 platform tags, and haven't even added any enemies or anything. I don't want to rely on string based programming to implement changes in my game, and right now I have a significant number of 'if' statements that are clogging up my code. Furthermore, I have some platforms that are mostly similar, but can perform slightly differently, such as passable blocks and passable icy blocks. Both allow the user to move freely through them, but obviously the icy block makes you slide around. \$\endgroup\$ – Ginger and Lavender Jan 23 '18 at 23:54
  • \$\begingroup\$ If it's 2d, then effectors should work \$\endgroup\$ – Bálint Jan 23 '18 at 23:54
  • \$\begingroup\$ Unfortunately I cannot use effectors because I am using a custom raycast controller with a kinematic ridigbody, and not Unity's built in physics. \$\endgroup\$ – Ginger and Lavender Jan 23 '18 at 23:56
  • 2
    \$\begingroup\$ Note that you should generally use collider.CompareTag(“platformPassable”) instead of collider.tag == “platformPassable” because it is faster than string comparison. \$\endgroup\$ – Ed Marty Jan 24 '18 at 1:32
3
\$\begingroup\$

Depending on the level of complexity required you could go one of two routes:

If you have a set of GameObjects that represent the different block types and all you need are a set of properties to pass into your player controller, then you could create a simple class that exposes those properties that you can add to the GameObject and create some prefabs for each type:

public class Platform : MonoBehaviour {
  public float AccelerationTimeGrounded;
  public float AccelerationTimeAirborne;
  public bool IsPassable;
}

The you can just add a "platform" tag to your platforms and grab the Platform component:

if (hit) {
  if (hit.collider.CompareTag("platform")) {
    var platform = hit.collider.gameObject.GetComponent<Platform>();

    // grab the properties as needed:
    Parameters.accelerationTimeGrounded = platform.AccelerationTimeGrounded;
    Parameters.accelerationTimeAirborne = platform.AccelerationTimeAirborne;
    Parameters.isPassable = platform.IsPassable;
  }
}

If however you need more complex requirements (for example you need to calculate some value in different ways depending on the type of platform - for example you want to multiply a value if it's an Ice platform, but only add a value if it's a Rock platform you can use an Interface within the scripts and then have the different types of platform implement them to pass the appropriate values to your player controller:

// Interface for all platform classes to inherit from.
interface IPlatform {
  float AccelerationTimeGrounded { get; }
  float AccelerationTimeAirborne { get; }
  bool IsPassable { get; }

  float FinalAcceleration (float current);
}

Then you can implement some concrete classes:

public class IcePlatform : IPlatform, MonoBehaviour {
   // Expose the properties to the editor with a reasonable default
   [SerializeField]
   private float _accelerationTimeGrounded = 1.0f;
   [SerializeField]
   private float _accelerationTimeAirborne = 3.0f;

   public float AccelerationTimeGrounded { get { return _accelerationTimeGrounded; } }
   public float AccelerationTimeAirborne { get { return _accelerationTimeAirborne; } }
   public bool IsPassable { get { return false; } }

   public bool FinalAcceleration (float current) {
     return current * AccelerationTimeGrounded;
   }
}

// Technically this could inherit from IcePlatform and just override
// the IsPassable property.
public class PassableIcePlatform : IPlatform, MonoBehaviour {
   // Expose the properties to the editor
   [SerializeField]
   private float _accelerationTimeGrounded = 1.0f;
   [SerializeField]
   private float _accelerationTimeAirborne = 3.0f;

   public float AccelerationTimeGrounded { get { return _accelerationTimeGrounded; } }
   public float AccelerationTimeAirborne { get { return _accelerationTimeAirborne; } }
   public bool IsPassable { get { return true; } }

   public bool FinalAcceleration (float current) {
     return current * AccelerationTimeGrounded;
   }
}

public class RockPlatform : IPlatform, MonoBehaviour {
   // Expose the properties to the editor with a reasonable default
   [SerializeField]
   private float _accelerationTimeGrounded = 0.5f;
   [SerializeField]
   private float _accelerationTimeAirborne = 1.0f;

   public float AccelerationTimeGrounded { get { return _accelerationTimeGrounded; } }
   public float AccelerationTimeAirborne { get { return _accelerationTimeAirborne; } }
   public bool IsPassable { get { return false; } }

   public bool FinalAcceleration (float current) {
     return current + AccelerationTimeGrounded;
   }
}

You can then use the Interface in your hit check without having to care about what type of IPlatform it is:

if (hit) {
  if (hit.collider.CompareTag("platform")) {
    // Get the component that implements IPlatform
    var platform = hit.collider.gameObject.GetComponent<IPlatform>();

    // grab the properties as needed:
    Parameters.accelerationTimeGrounded = platform.AccelerationTimeGrounded;
    Parameters.accelerationTimeAirborne = platform.AccelerationTimeAirborne;
    Parameters.isPassable = platform.IsPassable;

    // Call the method:
    float final = platform.FinalAcceleration(someCurrentValue);
  }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ I appreciate the level of detail in this answer; it is very helpful to me. I was just wondering if having to do var platform = hit.collider.gameObject.GetComponent<Platform>(); will be expensive in terms of performance. I've read in some places that GetComponent should be used sparingly, and my raycast system will be doing that pretty constantly. \$\endgroup\$ – Ginger and Lavender Jan 24 '18 at 16:39
  • 1
    \$\begingroup\$ @Dalsia - there are a couple of answers to that: 1. If CompareTag works for you and you don't need the control that the classes off you should be OK with the limit of 10001 tags within the Unity editor. 2. Measure it and find out - GetComponent<T> isn't as slow as some people think, so if what need is "what is under me now" you could consider optimisations such as stopping the check as soon as you've found a platform, or only checking every few frames. \$\endgroup\$ – Zhaph - Ben Duguid Jan 24 '18 at 17:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.