how i made the monster walk


if anyone learns from this and puts it into a game a shoutout would be nice and i'd love to play your game.

[note ] code isnt complete (you cant just copy paste it into your project  ) you will have to be able to understand the concepts and apply them to your own projects. 


 heres a video of the debug data which allowed me to see what was happening and will act as a reference to fill you in.

so to start we need an  ik solution something we can feed in a position or move an object around  for where the feet are and connect joints to  the body.  

if you don't have a way to set this up or just wanted to do your own ik solution like i did the basic idea is we move the joint position to the target with an offset of the length of the joint. and we do this forward down the chain of the joints and then the opposite way.

here's some basic code that will do that.

void SolveIk()
{
    for (int limbNum = 0; limbNum < limbs.Count; limbNum++)
    {
     //itterating from the foot to the part where it conects on the body. 
     // it should not move the foot or the body but they are in the array to allow for it to use there transform data
    for (int i = limbs[limbNum].Length - 2; i > 0; i--)  
        MoveLimb(limbs[limbNum][i], limbs[limbNum][i + 1], offsets[limbNum][i + 1]);
    // itterating starting from the anchor position back to the foot, doing this last keeps it connected to the body.    
    for (int i = 1; i < limbs[limbNum].Length - 1; i++) // itterating back from the 
        MoveLimb(limbs[limbNum][i], limbs[limbNum][i - 1], offsets[limbNum][i]);
    }
}
void MoveLimb(Transform limb, Transform target, float offset)  // basically clamp the distance to target limb keeping the angle between the 2
{
    // finds the angle from the limb to its target that being another limb
    // angle is in radians so to get the Euler rotation for sprites you'll need to convert to degrees
    float radian = Mathf.Atan2( limb.position.y- target.position.y, limb.position.x -target.position.x );
    // doing some trigonometry  we can use this angle and the offset length to find the displacement x and y position
    Vector3 posOffset = new Vector2(Mathf.Cos(radian), Mathf.Sin(radian)) * offset;
    Debug.DrawRay(target.position, posOffset,Color.yellow); // this gives us the yellow lines for our legs
    // then we set the final position to the targets position + the offset
    limb.position = target.position + posOffset;
 }


of course you need to rotate the game object to get rotation of the sprites to work but that's not that hard.

now we have an ik solution you should be able to move the legs around by the 2 end transforms

i'm cheating and running a fixed update saying that the body position is the average of its feet placement + a Vector2.up. and  a head ik target that will go to the player but clamped by limiting its distance this is so the head isnt fully outstretched when the player is far away.

since the ik is in place, the legs  could be puppet-ed to move manually. but we want it to be able towalk on terrain. and we dont want to animate the foot positions we want something that we can just throw onto any terrain and it to look decent.  

first we need to set up some ideal position for the foot to be. i just made a game object and parented it to the body, neck and head of my monster which with the head ik meant that the if the player id behind the monster it will start trying to move backwards.

now we start a coroutine that will handle picking valid positions on our ground. we do this by raycasting down and if that  returns false we rotate the angle to target position based on the iteration we are currently on in such fashion that it checks left and right depending on if the iteration/2 number is a whole number or not.

then we calculate a path an physically simulated object would take to get to that position this is the green curve that the feet follow, collecting those positions, stopping if it hits anything on the way, then sends them to a coroutine that iteratates through that list moving the foot to those positions with a delay corisponding to the time.fixeddeltatimestep in order to move the foot to that position.  during the movment we set a "can continue" bool to true a single time and the main coroutine starts running the  next foot.

   code below 

IEnumerator DoLegMovement()
    { 
        while (true) // this goes on forever 
        {
            for (int i = 0; i < feetIKTargets.Length; i++)
            {
                canContinue = false;
                RaycastHit2D[] hits;
                Vector2 targetPosition = idealFootPosition[i].position;
                Vector2 footPosition = feetIKTargets[i].position;
                for (int ii = 0; ii < 10; ii++)
                {
                    hits = new RaycastHit2D[1];// this is our output for valid positions. 
                    float angleToTargetPosition = Mathf.Atan2(-1, 0) +(ii / 2 == ii / 2f ? -1 : 1) * ii * 0.2f; // so it rotates the search back and foward getting higher the longer it goes    
                    Vector2 rayDirection = new Vector2(Mathf.Cos(angleToTargetPosition), Mathf.Sin(angleToTargetPosition));
                    
                    Physics2D.Raycast(targetPosition, rayDirection, contactFilterForGround, hits, rayLength);// raycasts to the ground beneath it 
                    //draws a red line if it doesnt hit anything and a green one if it does.
                    Debug.DrawLine(targetPosition, (!hits[0]) ? (targetPosition + (rayDirection * rayLength)) : hits[0].point, !hits[0] ? Color.red : Color.green, 1f);
                    
                    if (hits[0])// if it hits something 
                        if (TryMoveFootToPosition(hits[0].point, feetIKTargets[i], angleToTargetPosition)) // try move the foot to the new position 
                        {
                            yield return new WaitUntil(() => canContinue); // if its moving the foot then wait till the foot hits something       
                            ii = 20; // breaks out of the loop / stops the search for new leg positions
                        }
                }
                yield return new WaitForEndOfFrame();
            }
            yield return new WaitForEndOfFrame();
        }
bool TryMoveFootToPosition(Vector2 target, Transform foot, float searchAngle)// checks if the path to this position through a midpoint is valid and if so will start the foot movement returns false if its invalid.
        // finds a mid point in order to move the positons
        Vector2 midpoint = foot.position + (new Vector3(Mathf.Cos(searchAngle), Mathf.Sin(searchAngle)));            
        midpoint = ((target + (Vector2)foot.position) / 2f) + Vector2.up * 1.4f;
        // draws a triagle from between all points so i know which midpoint is where and what leg it attaches to
        Debug.DrawLine(foot.position, midpoint, Color.magenta, 0.05f); 
        Debug.DrawLine(foot.position, target, Color.green, 0.05f); 
        Debug.DrawLine(midpoint, target, Color.red, 0.05f); 
        Vector2[] positions = GetCurveToTarget(foot.position , midpoint, target); // gets the points which the foot will move through
        if (positions != null)
        {
            StartCoroutine(MoveFoot(foot , positions , Time.fixedDeltaTime)); // moves the feet through a list of positions prevously grabbed
            return true;
        }
        return false;
    }
    IEnumerator MoveFoot(Transform foot ,Vector2[] positions,  float timeStep = 0.1f) // moves the foot through the array of positions with the apropriate timestep
    {
        int halfLength = positions.Length / 2;
        for (int i = 0; i < positions.Length; i++)
        {
            foot.transform.position = positions[i];
            yield return new WaitForSeconds(timeStep);
            if (i == halfLength) 
                canContinue = true; 
        }
        footContactSound.Play();
        cameraController.ScreenShake(foot.position, 10);
    }
    // this is just so i could reuse code from another game i was developing
    public Vector2[] GetCurveToTarget(Vector2 footPos,  Vector2 midpoint, Vector2 target)
    {
        return DrawPhysicsCurveFromPositionToPosition(footPos, midpoint, target,Time.fixedDeltaTime *speed);
    }
    
    Vector2[] DrawPhysicsCurveFromPositionToPosition(Vector2 origin, Vector2 midpoint, Vector2 target, float timeStep = 0.1f)
    {
        if (  target.x > origin.x && midpoint.x < ((target.x + origin.x) / 2f)      || target.x < origin.x && midpoint.x > ((target.x + origin.x) / 2f)  )
        {    
            // cansels the move if the midpoint isnt where it should be
            Debug.DrawRay(midpoint, Vector2.up, Color.yellow, 0.1f);
            return null;
        } 
       // does some math to work out the forces needed to pass an object of mass 1 through the midpoint to the target
        Vector2 P1 = (midpoint - origin);        //point above the net - player position(reached when t = t1) // as we are shooting from zero this will still be one 
        Vector2 P2 = (target - origin);         // target point - player position(reached when t = t2)
        Vector2 g = Physics2D.gravity;         //gravitational acceleration vector
        float t2 = Mathf.Sqrt(2 * (P2.y - P1.y * P2.x / P1.x) / (g.y * (1 - P1.x / P2.x))); // returns time for the ypos
        Vector2 v = P2 / t2 - t2 * g / 2f; //  gets the velosity needed to move the object
        //  should remove divisable by zero errors. 
        if (v.x == Mathf.Infinity || v.x == Mathf.NegativeInfinity || v.y == Mathf.Infinity || v.y == Mathf.NegativeInfinity || v.x == 0 || v.y == 0)
            return null;
        
        return DrawAndGetPositions(origin, v, timeStep, 5); // does the math to get the positons and draws the green curve representing the feet position
    }
    Vector2[] DrawAndGetPositions(Vector2 throwPos, Vector2 f, float timeStep, int maxTime) // creates a list of positions based on the timestep and the inital velocity. collects everything into a list and then returns it for the other things to use 
    {
        RaycastHit2D[] hits = new RaycastHit2D[1];
        Vector2 startPosition = throwPos;
        Vector2 endPosition = Vector2.zero;
        List<vector2> positionLog = new List<vector2>();
        for (float t = timeStep; t < maxTime; t += timeStep)
        {
            endPosition = throwPos + new Vector2((f.x * t), (f.y * t) + ((Physics2D.gravity.y * (t * t)) / 2f));
            if (Physics2D.Raycast(startPosition, endPosition - startPosition, contactFilterForGround, hits, (startPosition - endPosition).magnitude) > 0)
            {               
                endPosition = hits[0].point + offset;
                Debug.DrawRay(startPosition, endPosition - startPosition, Color.red, 3f); // when it hits something it draws a red line to that point
                positionLog.Add(endPosition);
                break;
            }
            else
            Debug.DrawRay(startPosition, endPosition - startPosition, Color.green, 3f);// draws a green line if it doesnt hit anything
            
            positionLog.Add(endPosition);
            startPosition = endPosition;
        }
        return positionLog.ToArray();
    }</vector2></vector2>
    }


the monster had a tenancy to cling to walls a bit so i made it the last position  offset from the hit point so that it can go through the walls meaning its a lot quicker which is useful when you want the chase to be chalenging.

Files

Latest Build 31 MB
May 05, 2020
original GameJam submission build 31 MB
May 03, 2020
webgl build 1.1 .zip 29 MB
May 05, 2020

Get Antiverous

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.