Adam's Lair Forum

game development and casual madness
It is currently 2017/04/29, 13:20

All times are UTC + 1 hour [ DST ]




Post new topic Reply to topic  [ 7 posts ] 
Author Message
 Post subject: sloped, uneven ground?
PostPosted: 2017/02/25, 19:56 
Junior Member
Junior Member

Joined: 2017/02/05, 13:06
Posts: 29
Role: Hobbyist
Another question. I have an uneven ground with various hills and valleys. I want the character to be able to move fairly smoothly over the ground. I went into the rigid body editor and made a polygon shape which matches (fairly closely) the contour of the ground. As the character is moved from high ground to low it floats in mid-air for a second then gradually gravity starts taking over (gravity is set at default)
As the character tries to go from low to high ground it stutters as if it takes a step, stubs its toe and stops, then takes another step.

I've tried to simplify the ground to see what's going on so right now I just have a single triangle:

https://drive.google.com/open?id=0B5yBQ328r9F4NHItdGdXaTVwLUU

Here is the result:
https://youtu.be/vprMxdQVEZU

I tried turning the friction down to 0 but that just makes the ground like ice so I need friction.

Maybe the problem is in how the character is moving, Right now if a move key is pressed then the linear velocity is set to a fixed value. Should I be re-positioning the character rather than adjusting the velocity? If so, how do I make sure the character stays above the ground and not in it?


Top
 Profile  
 
PostPosted: 2017/02/25, 21:28 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 1949
Location: Germany
Role: Professional
Hey there,

based on what it sounds like, the problem is that you're essentially slamming your character across the ground, hard edge vs. hard edge. That's not something where you'd expect smooth movement as a result.

Divarin wrote:
Maybe the problem is in how the character is moving, Right now if a move key is pressed then the linear velocity is set to a fixed value. Should I be re-positioning the character rather than adjusting the velocity? If so, how do I make sure the character stays above the ground and not in it?


Using linear velocity is definitely not a bad choice if you're using physics for collision behavior. Setting the position instead would, from the perspective of the physics system, teleport the character around in small increments, so you will get unexpected behavior there, and it's an inconsistency with the potential to complicate things in the long run.

What you could do is create a character with a ball-shaped base, so there is no hard edge that could get stuck somewhere. All else is sort of experimental: There have been approaches where that ball is actually a different object that is connected to the main body using a wheel joint, so it can rotate freely (like a wheel), and that is rotated for movement, rather than applying linear velocity directly. Alternatively, you could keep it as part of the main body and adjust friction based on whether or not the character is moving, though you'd be back with directly applied velocity with this.

I'm pretty sure there are other approaches too, platformer character controls are actually a tricky topic that sometimes requires a lot of tweaking - so whatever your results are, I think sharing them when you got it working might help a few people around here. ^^

Edit: Oh, another idea: You could make the linear velocity vector that you apply match the slope of the ground you're currently standing on, so it's slightly downward / upward depending on the ground. You can get collision information such as the normal of the collision in the resolve collision handler!

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2017/02/25, 22:08 
Junior Member
Junior Member

Joined: 2017/02/05, 13:06
Posts: 29
Role: Hobbyist
Thanks I think that might have gotten me on the right track. Making the character's bottom half a circle definitely helped a lot but it's still a little sticky going up hill. I will play with the collision data and see if I can refine it.


Top
 Profile  
 
PostPosted: 2017/02/26, 13:29 
Junior Member
Junior Member

Joined: 2017/02/05, 13:06
Posts: 29
Role: Hobbyist
Well got back into it today. It looks like the "CollisionData" object in the CollisionEventArgs (during OnCollisionBegin) is always null. However, I can get it OnCollisionSolve.


Top
 Profile  
 
PostPosted: 2017/03/02, 00:12 
Junior Member
Junior Member

Joined: 2017/02/05, 13:06
Posts: 29
Role: Hobbyist
Alright well I thought I'd follow up with this. After doing some more searching I found http://www.adamslair.net/forum/viewtopic.php?f=4&t=939&start=10 which led me to https://www.youtube.com/playlist?list=PLFt_AvWsXl0f0hqURlhyIoAabKPgRsqjz which was a very enlightening video series.

The person who put that together began porting it to Duality so I started with that as a base and continued from there following along with his video series. I got as far as episode 5 (descending slopes) but stopped short of moving platforms, wall-jumping etc as they are not needed for my game. I also had to change some of the variable values quite dramatically from what he used in Unity but I put the values that worked for me as the default values in the code I will show below.

To anyone reading this in the future, you may need to tweak these values.

The code consists of two controllers one called "RaycastController" (which is what the YouTuber calls "Controller2D") and "PlayerController"

The PlayerController has a required component of RaycastController so when you add the PlayerController to your player sprite you'll get the RaycastController as well.

Also note: Disregard the "EventAggregator" lines as that's just an event handling system specific to my game.

PlayerController:
Code:
    [RequiredComponent(typeof(RaycastController))]
    public class PlayerController : Component, ICmpUpdatable, ICmpInitializable
    {
        public float MoveSpeed { get; set; } = 1500f;
        public float JumpHeight { get; set; } = 500f;
        public float TimeToJumpApex { get; set; } = 0.04f;
        public float AccelerationGrounded { get; set; } = 100f;
        public float AccelerationAirborne { get; set; } = 25f;

        private float gravity;
        private float jumpVelocity;
        private bool _isJumping = false;

        private Vector2 velocity;

        private RaycastController RaycastController
        {
            get
            {
                return GameObj.GetComponent<RaycastController>();
            }
        }

        public void OnUpdate()
        {
            if (RaycastController.Collisions.below || RaycastController.Collisions.above)
                velocity.Y = 0;

            Vector2 input = Vector2.Zero;
            if (DualityApp.Keyboard[Key.Left] || DualityApp.Keyboard[Key.A])
                input.X = -1;
            else if (DualityApp.Keyboard[Key.Right] || DualityApp.Keyboard[Key.D])
                input.X = 1;
            //if (DualityApp.Keyboard[Key.Up])
            //    input.Y = -1;
            //else if (DualityApp.Keyboard[Key.Down])
            //    input.Y = 1;

            // Don't use "KeyHit" as it may get skipped occasionally. check if key is down (regardless of whether or not it was hit this frame)
            // and then check if the character is already jumping from a previous frame (_isJumping)
            if ( !_isJumping && (DualityApp.Keyboard.KeyPressed(Key.Space) || DualityApp.Keyboard.KeyPressed(Key.Up)))
            {
                velocity.Y = -jumpVelocity;
                _isJumping = true;
            }
            else if (_isJumping && RaycastController.Collisions.below)
            {
                _isJumping = false;
            }

            float targetVelocityX = input.X * MoveSpeed;

            if (RaycastController.Collisions.below)
                velocity.X = MathF.Lerp(velocity.X, targetVelocityX, AccelerationGrounded / 100);
            else
                velocity.X = MathF.Lerp(velocity.X, targetVelocityX, AccelerationAirborne / 100);

            velocity.Y += gravity * Time.TimeMult / 60f;
            RaycastController.Move(velocity * Time.TimeMult / 60f);
            EventAggregator.AnnounceEvent(new PlayerMovedEvent(GameObj.Transform.Pos));
        }

        public void OnInit(InitContext context)
        {
            gravity = (2 * JumpHeight) / TimeToJumpApex / TimeToJumpApex;
            jumpVelocity = MathF.Abs(gravity) * TimeToJumpApex;
            Log.Game.Write(String.Format("Gravity: {0}, jump velocity: {1}", gravity, jumpVelocity));
        }

        public void OnShutdown(ShutdownContext context)
        {
        }
    }


And now for the big one, RaycastController:

Code:
    public class RaycastController : Component, ICmpInitializable
    {
        public float SkinWidth { get; set; } = 10f;
        public int HorizontalRayCount { get; set; } = 4;
        public int VerticalRayCount { get; set; } = 4;
        public float MaxClimbAngle { get; set; } = 80f;
        public float MaxDescendAngle { get; set; } = 75f;

        private const float DEBUG_RAYCAST_VECTOR_DISPLAY_LENGTH_MULTIPLIER = 10;
        private const bool SHOW_DEBUG_MESSAGES = false;

        private CollisionInfo _collisions;
        public CollisionInfo Collisions
        {
            get
            {
                return _collisions;
            }
        }

        private float _horizontalRaySpacing;
        private float _verticalRaySpacing;

        private Rect _bounds;
        private RayCastOrigins _raycastOrigins;

        public void OnInit(InitContext context)
        {
            CalculateBounds();
            CalculateRaySpacing();           
        }

        public void OnShutdown(ShutdownContext context) { }

        public void Move(Vector2 velocity)
        {
            CalculateBounds();
            CalculateRayCastOrigins();
            _collisions.Reset();

            if (float.IsNaN(velocity.X))
                velocity.X = 0;
            if (float.IsNaN(velocity.Y))
                velocity.Y = 0;

            if (velocity.Y > 0)
                DescendSlope(ref velocity);

            if (velocity.X != 0)
                HorizontalCollisions(ref velocity);

            if (velocity.Y != 0)
                VerticalCollisions(ref velocity);
           
            GameObj.Transform.MoveByAbs(velocity);
        }

        private void HorizontalCollisions(ref Vector2 velocity)
        {
            float directionX = MathF.Sign(velocity.X);
            float rayLength = MathF.Abs(velocity.X) + SkinWidth;

            for (int i = 0; i < HorizontalRayCount; i++)
            {
                Vector2 rayOrigin = (directionX == -1) ? _raycastOrigins.bottomLeft : _raycastOrigins.bottomRight;
                rayOrigin -= Vector2.UnitY * (_horizontalRaySpacing * i);
               
                if (DualityApp.ExecEnvironment == DualityApp.ExecutionEnvironment.Editor)
                    VisualLog.Default.DrawVector(rayOrigin.X, rayOrigin.Y, 0, directionX * rayLength * DEBUG_RAYCAST_VECTOR_DISPLAY_LENGTH_MULTIPLIER, 0);

                RayCastCallback raycastCallback = data => 1.0f;
                RayCastData rayCastData;

                if (RigidBody.RayCast(rayOrigin, rayOrigin + Vector2.UnitX * directionX * rayLength, raycastCallback, out rayCastData))
                {
                    var distance = (rayOrigin - rayCastData.Pos).Length;
                    float slopeAngle = Vector2.AngleBetween(rayCastData.Normal, Vector2.UnitY * -1);
                    slopeAngle = MathF.RadToDeg(slopeAngle);
                    if (i == 0 && slopeAngle <= MaxClimbAngle)
                    {
                        float distanceToSlopeStart = 0;
                        if (slopeAngle != _collisions.slopeAngleOld)
                        {
                            distanceToSlopeStart = distanceToSlopeStart - SkinWidth;
                            velocity.X -= distanceToSlopeStart * directionX;
                        }
                        ClimbSlope(ref velocity, slopeAngle);
                        velocity.X += distanceToSlopeStart * directionX;
                    }

                    if (_collisions.climbingSlope || slopeAngle > MaxClimbAngle)
                    {
                        velocity.X = (distance - SkinWidth) * directionX;
                        rayLength = distance;

                        //if (_collisions.climbingSlope)
                        //{
                        //    velocity.Y = -1 * MathF.Tan(MathF.DegToRad(_collisions.slopeAngle)) * MathF.Abs(velocity.X);
                        //}

                        _collisions.right = directionX == 1;
                        _collisions.left = directionX == -1;

                        if (SHOW_DEBUG_MESSAGES)
                        {
                            if (_collisions.right)
                                EventAggregator.AnnounceEvent(new DebugMessageEvent("Collision right"));
                            if (_collisions.left)
                                EventAggregator.AnnounceEvent(new DebugMessageEvent("Collision left"));
                        }
                    }
                }
            }
        }

        private void VerticalCollisions(ref Vector2 velocity)
        {
            float directionY = MathF.Sign(velocity.Y);
            float rayLength = MathF.Abs(velocity.Y) + SkinWidth;

            for (int i = 0; i < VerticalRayCount; i++)
            {
                Vector2 rayOrigin = (directionY == -1) ? _raycastOrigins.topLeft : _raycastOrigins.bottomLeft;
                rayOrigin += Vector2.UnitX * (_verticalRaySpacing * i + velocity.X);

                if (DualityApp.ExecEnvironment == DualityApp.ExecutionEnvironment.Editor)
                    VisualLog.Default.DrawVector(rayOrigin.X, rayOrigin.Y, 0, 0, directionY * rayLength * DEBUG_RAYCAST_VECTOR_DISPLAY_LENGTH_MULTIPLIER);

                RayCastCallback raycastCallback = data => 1.0f;
                RayCastData rayCastData;

                if (RigidBody.RayCast(rayOrigin, rayOrigin + Vector2.UnitY * directionY * rayLength, raycastCallback, out rayCastData))
                {
                    var distance = (rayOrigin - rayCastData.Pos).Length;
                    velocity.Y = (distance - SkinWidth) * directionY;
                    rayLength = distance;

                    //if (_collisions.climbingSlope)
                    //{
                    //    velocity.X = velocity.Y / MathF.Tan(MathF.DegToRad(_collisions.slopeAngle)) * MathF.Sign(velocity.X);
                    //}

                    _collisions.below = directionY == 1;
                    _collisions.above = directionY == -1;
                    if (SHOW_DEBUG_MESSAGES)
                    {
                        if (_collisions.below)
                            EventAggregator.AnnounceEvent(new DebugMessageEvent("Collison below"));
                        if (_collisions.above)
                            EventAggregator.AnnounceEvent(new DebugMessageEvent("Collision above"));
                    }
                }
            }
           
        }

        private void ClimbSlope(ref Vector2 velocity, float slopeAngle)
        {
            float moveDistance = MathF.Abs(velocity.X);
            var slopeRads = MathF.DegToRad(slopeAngle);

            float climbVelocityY = MathF.Sin(slopeRads) * moveDistance * -1;

            // if walking on slope rather than jumping on slope
            if (velocity.Y >= climbVelocityY)
            {
                velocity.Y = velocity.Y = climbVelocityY;
                velocity.X = MathF.Cos(slopeRads) * moveDistance * MathF.Sign(velocity.X);
                _collisions.below = true;
                _collisions.climbingSlope = true;
                _collisions.slopeAngle = slopeAngle;

                if (SHOW_DEBUG_MESSAGES)
                    EventAggregator.AnnounceEvent(new DebugMessageEvent($"Climbing slope at {slopeAngle} deg."));

                if (DualityApp.ExecEnvironment == DualityApp.ExecutionEnvironment.Editor)
                    VisualLog.Default.DrawVector(GameObj.Transform.Pos.X, GameObj.Transform.Pos.Y, GameObj.Transform.Pos.Z, velocity.X * DEBUG_RAYCAST_VECTOR_DISPLAY_LENGTH_MULTIPLIER, velocity.Y * DEBUG_RAYCAST_VECTOR_DISPLAY_LENGTH_MULTIPLIER);
            }
        }

        private void DescendSlope(ref Vector2 velocity)
        {
            float directionX = MathF.Sign(velocity.X);
            Vector2 rayOrigin = (directionX == -1) ? _raycastOrigins.bottomRight : _raycastOrigins.bottomLeft;

            RayCastCallback raycastCallback = data => 1.0f;
            RayCastData rayCastData;

            if (RigidBody.RayCast(rayOrigin, rayOrigin + Vector2.UnitY * float.MaxValue, raycastCallback, out rayCastData))
            {
                float slopeRadians = Vector2.AngleBetween(rayCastData.Normal, Vector2.UnitY * -1);
                var distance = (rayOrigin - rayCastData.Pos).Length;
                float slopeAngle = MathF.RadToDeg(slopeRadians);

                if (slopeAngle != 0 && slopeAngle <= MaxDescendAngle)
                {
                    if (MathF.Sign(rayCastData.Normal.X) == directionX)
                    {
                        if (distance - SkinWidth <= MathF.Tan(slopeRadians) * MathF.Abs(velocity.X))
                        {
                            float moveDistance = MathF.Abs(velocity.X);
                            float descentVelocityY = MathF.Sin(slopeRadians) * moveDistance;
                            velocity.X = MathF.Cos(slopeRadians) * moveDistance * MathF.Sign(velocity.X);
                            velocity.Y += descentVelocityY;

                            _collisions.slopeAngle = slopeAngle;
                            _collisions.descendingSlope = true;

                            if (SHOW_DEBUG_MESSAGES)
                                EventAggregator.AnnounceEvent(new DebugMessageEvent($"Descending slope at {slopeAngle} deg."));

                            if (DualityApp.ExecEnvironment == DualityApp.ExecutionEnvironment.Editor)
                                VisualLog.Default.DrawVector(GameObj.Transform.Pos.X, GameObj.Transform.Pos.Y, GameObj.Transform.Pos.Z, velocity.X * DEBUG_RAYCAST_VECTOR_DISPLAY_LENGTH_MULTIPLIER, velocity.Y * DEBUG_RAYCAST_VECTOR_DISPLAY_LENGTH_MULTIPLIER);
                        }
                    }
                }
            }
        }

        private void CalculateBounds()
        {
            var spriteRenderer = GameObj.GetComponent<SpriteRenderer>();
            var animSpriteRenderer = GameObj.GetComponent<AnimSpriteRenderer>();
            if (spriteRenderer != null)
            {
                _bounds = new Rect(
                    GameObj.Transform.Pos.X + spriteRenderer.Rect.X,
                    GameObj.Transform.Pos.Y + spriteRenderer.Rect.Y,
                    spriteRenderer.Rect.W,
                    spriteRenderer.Rect.H);
            }
            else if (animSpriteRenderer != null)
            {
                _bounds = new Rect(
                    GameObj.Transform.Pos.X + animSpriteRenderer.Rect.X,
                    GameObj.Transform.Pos.Y + animSpriteRenderer.Rect.Y,
                    animSpriteRenderer.Rect.W,
                    animSpriteRenderer.Rect.H);
            }
            else {
                Log.Game.WriteError("A spriterenderer or animspriterenderer has to be attached!");
                _bounds = new Rect();
            }
        }

        private void CalculateRayCastOrigins()
        {
            var shrinkedBounds = new Rect(_bounds.X + SkinWidth, _bounds.Y + SkinWidth, _bounds.W - 2 * SkinWidth, _bounds.H - 2 * SkinWidth);
            _raycastOrigins.topLeft = shrinkedBounds.TopLeft;
            _raycastOrigins.bottomLeft = shrinkedBounds.BottomLeft;
            _raycastOrigins.topRight = shrinkedBounds.TopRight;
            _raycastOrigins.bottomRight = shrinkedBounds.BottomRight;
        }

        private void CalculateRaySpacing()
        {
            var shrinkedBounds = new Rect(_bounds.X + SkinWidth, _bounds.Y + SkinWidth, _bounds.W - 2 * SkinWidth, _bounds.H - 2 * SkinWidth);

            HorizontalRayCount = MathF.Clamp(HorizontalRayCount, 2, int.MaxValue);
            VerticalRayCount = MathF.Clamp(VerticalRayCount, 2, int.MaxValue);

            _horizontalRaySpacing = shrinkedBounds.H / (HorizontalRayCount - 1);
            _verticalRaySpacing = shrinkedBounds.W / (VerticalRayCount - 1);
        }

        public struct CollisionInfo
        {
            public bool above, below, left, right;           
            public float slopeAngle, slopeAngleOld;
            public bool climbingSlope;
            public bool descendingSlope;

            public void Reset()
            {
                above = below = left = right = false;
                slopeAngleOld = slopeAngle;
                climbingSlope = descendingSlope = false;
                slopeAngle = 0;
            }
        }

        private struct RayCastOrigins
        {
            public Vector2 topLeft, bottomLeft, topRight, bottomRight;
        }
    }


For the scene I used no rigid body at all on the character and for the ground I used Chain Shape which just follows the visible contour of the ground.


Top
 Profile  
 
PostPosted: 2017/03/02, 18:34 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 1949
Location: Germany
Role: Professional
Nice followup post - pretty sure that could help some people in the future :+1: :)

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2017/03/09, 10:33 
Junior Member
Junior Member

Joined: 2016/05/15, 18:27
Posts: 45
Role: Hobbyist
Hey,

I have a functioning character controller for whoever wants it. It works with slopes. The both scripts must be placed on the player object :D


There's some hacky animation stuff in this one that you can get rid of:
http://pastebin.com/tkKYA0fy

This script does most of the calculating:
http://pastebin.com/LexmTNVf


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 7 posts ] 

All times are UTC + 1 hour [ DST ]


Who is online

Users browsing this forum: No registered users and 4 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Jump to:  
Powered by phpBB® Forum Software © phpBB Group