Character Controller: Gravity, Scaling, Slopes! Oh my!

I have been tasked to do a fairly unique public art installation for a client using Unity, in which we will be displaying the 3D environment on a large wall size display. We don’t want to use a traditional perspective camera, as it presents the best view from a single fixed point in space. Instead we are using an orthographic camera (which will provide us with scalability on future projects if we want to use multiple cameras stitched to present one long scene) with a variety of visual tricks to give the viewer a sense of depth.

You can see from the image that two of the tricks being used are (1) scaling of the characters given their z-position in space away from the camera, and (2) an inclined floor to give the appearance of diminishing perspective. However these two things have brought me on to another issue, which is keeping the characters ON the sloped surface. They will either immediately fall through, or walk around briefly and then change directions and fall through. I have tried adjusting the slope value and skin value in the character controller, but can’t tell if it’s making any difference.

Has anyone had issues with characters falling through ramps?
Has anyone had issues with scaling characters and colliders being affected by this?
Is there a better way to add gravity than simply subtracting a value from the y position when isGrounded is false?

BTW, the crazy low color depth is just a shader I’ve been trying out. Your eyes and monitor are fine.

Ok, I’m bumping this thread with code this time, because this is seriously holding up development on this project. A lot of this code is simply used for animation blending and syncing of animation state over the network. The key aspects are in the Update() and scaleAvatar() functions:

using UnityEngine;
using System.Collections;

public class AvatarAnimator : Avatar {
	
	/*
	* AvatarAnimator.cs
	* by Aaron Siegel, 6-21-2010
	*
	* Acts as animation blend controller as well as network view sync and RPC call syncing.
	*/
	
	public float walkSpeed = 10f;//2.3f;
	public float runSpeed = 20f;//3.6f;
	public float idleThreshold = 0.3f;
	public float speedSmoothing = 8.0f;
	public float gravity = 10.0f;
	
	private AnimationState run;
	private AnimationState walk;
	private float currentSpeed = 0.0f;
	private float smoothedSpeed = 0.0f;
	private Vector3 lastPos;
	private Vector3 pos;
	
	// scaling variables
	float maxScale = 7500.0f;
	float minScale = 5000.0f;
	float currentScale;
	float fullZspace = 366;
	float zscale;
	
	private bool client = false;

	void Start () {
		animations = new string[2];
		// We are in full control here - don't let any other animations play when we start
		animation.Stop();

		// By default loop all animations
		animation.wrapMode = WrapMode.Loop;
		
		// setup list of animations for reference when setting current animation via RPC
		animations[0] = "walk";
		animations[1] = "runSlow";
		walk = animation["walk"];
		run = animation["runSlow"];
		
		// Enable all walk / run cycles 
		// We will manually adjust the blend weights every frame
		walk.enabled = true;
		run.enabled = true;
		
		// Synchronize all walk / run cycle animations
		animation.SyncLayer(0);

		// Put the idle animation in a lower layer.
		// This will make it only play if no walk / run cycle is faded in
		animation["idle"].layer = -1;
		animation["idle"].enabled = true;
		animation["idle"].weight = 1;
	
		// discover if this instance is a client or the server
		GameObject conductor = GameObject.Find("Conductor");
		client = System.Convert.ToBoolean((conductor.GetComponent("Conductor") as Conductor).props.getProperty("client"));
	}
	
	// this provides interaction with rigidbody objects or other colliders
	void OnControllerColliderHit(ControllerColliderHit hit){
		float pushPower = 2.0f;
		Rigidbody body = hit.collider.attachedRigidbody;
		if(body == null || body.isKinematic){
			return;
		} else if(hit.moveDirection.y < -0.3){
			return;
		}
		Vector3 pushDir = new Vector3(hit.moveDirection.x, 0, hit.moveDirection.z);
		body.velocity = pushDir * pushPower;
	}
	
	// this serializes the current animation state and sends it out at a steady rate
	void OnSerializeNetworkView (BitStream stream, NetworkMessageInfo info) {
		float animTime = 0;
		if (stream.isWriting){
			animTime = animation[animations[currentAnimation]].normalizedTime;
			stream.Serialize(ref animTime);
		} else {
			stream.Serialize(ref animTime);
			animation[animations[currentAnimation]].normalizedTime = animTime;
		}
	}
	
	[RPC]
	void setAvatarPosition(Vector3 pos){
		lastPos = transform.position;
		transform.position = pos;
		//this.pos = pos;
	}
	
	[RPC]
	void setAvatarRotation(Quaternion rot){
		transform.rotation = rot;
	}
	
	[RPC]
	void setAvatarSpeed(float newSpeed){
		currentSpeed = newSpeed;
	}
	
	[RPC]
	void setAnimationState(int animState){
		currentAnimation = animState;
	}
	
	[RPC]
	void setAnimationPosition(float normPos){
		animation[animations[currentAnimation]].normalizedTime = normPos;
	}
	
	void scaleAvatar(){
		Transform curTransform = (Transform) GetComponent("Transform");
		zscale = (fullZspace - transform.position.z) / fullZspace;
		currentScale = ((maxScale - minScale) * zscale) + minScale;
		//print(transform.position.z +" "+ currentScale);
		curTransform.localScale = new Vector3(currentScale, currentScale, currentScale);
	}
	
	void Update () {
		if(!client){
			// check person object position for latest tracking data
			lastPos = transform.position;
			pos = person.getPosition();
			
			// scale avatar to fake depth effect
			scaleAvatar();
			
			// look towards new position
			Vector3 offset = pos - transform.position;
			offset.y = 0;	// ignore Y height so character doesn't rotate along the X axis
			if(offset.x != 0  offset.z != 0){
				transform.rotation = Quaternion.LookRotation(offset,Vector3.up); 
				// move to new position
				currentSpeed = Vector3.Distance(pos, lastPos);
				CharacterController cc = (CharacterController)GetComponent ("CharacterController");
				Vector3 forward = transform.TransformDirection(Vector3.forward);
				cc.SimpleMove(forward * currentSpeed );
				if(!cc.isGrounded){
					transform.position = new Vector3(transform.position.x, transform.position.y - (gravity * Time.deltaTime), transform.position.z);
				}
				networkView.RPC("setAvatarPosition", RPCMode.All, transform.position);
				networkView.RPC("setAvatarRotation", RPCMode.All, transform.rotation);
				networkView.RPC("setAvatarSpeed", RPCMode.All, currentSpeed);
			}
			
		}
			
		smoothedSpeed = Mathf.Lerp(smoothedSpeed, currentSpeed, Time.deltaTime * speedSmoothing);
		// Calculate the weight between walk and run from the current speed
		float runWeight = Mathf.InverseLerp(walkSpeed, runSpeed, Mathf.Abs(smoothedSpeed));
		// adjust the animation playback to match the characters speed
		run.speed = smoothedSpeed / runSpeed;
		walk.speed = smoothedSpeed / walkSpeed;
		
		// When the character slows down fade out the run and walk weights
		if (Mathf.Abs(smoothedSpeed) < idleThreshold){
			run.weight  -= Time.deltaTime * 5.0f;
			walk.weight -= Time.deltaTime * 5.0f;
		} else {
			float totalWeight = run.weight + walk.weight;
			totalWeight += Time.deltaTime * 12;
			totalWeight = Mathf.Clamp01(totalWeight);

			// Set the weights based on the current speed
			run.weight = runWeight * totalWeight;
			walk.weight = (1.0f - runWeight) * totalWeight;
		}
	
	}
	
}

The Avatar class is just a simple generic super class extending MonoBehavior to be used with the 3D characters in the scene, and I don’t think it is of much concern while assessing this issue:

using UnityEngine;
using System.Collections;

public class Avatar : MonoBehaviour {

	/*
	* Avatar.cs
	* by Aaron Siegel, 6-10-2010
	*
	* Super class for all 3D model attributes used to interact with one or more person objects.
	*/
	
	protected int currentAnimation = 0;
	public string[] animations;
	protected Person person;
	
	public void setPerson(Person person){
		this.person = person;
	}
	
	public void setPosition(Vector3 pos){
		transform.position = pos;
	}
	
	public void setRotation(Quaternion rot){
		transform.rotation = rot;
	}
	
	public void setAnimationState(int animState){
		currentAnimation = animState;
	}
	
	public void setAnimationPosition(float normPos){
		animation[animations[currentAnimation]].normalizedTime = normPos;
	}
}

I am positive that this has something to do with the order of operations. I noticed a lot of scripts applying gravity and movement to character controllers inside FixedUpdate instead of Update.

The problems I’m encountering are:
(1) If I give them no gravity and they’re too high, they just walk in place in the sky and never go anywhere.
(2) If I give them gravity, they usually end up falling completely through the ground.
(3) If I don’t give them gravity and start them lower in the hopes of them colliding with the ramp to begin with, they usually just intersect and stay stuck in place.

Any advice would be GREATLY appreciated.

The scaling might be what is causing the problem here. Try putting the character’s mesh on a child object and then scaling that rather than the parent object with the CharacterController.

I disabled the scaling entirely to see if that would fix it. It seems like it is a little more reliable on making contact with the sloped surface and walking on it, but it’s still unpredictable and ends up either falling directly through or walking off in space.

So what is happening here is a direct result of either the incline of the slope, the rate at which the character controller is falling, or a combination of both. I have read that the skin width makes a big impact on this type of behavior, but I’m having difficulty even observing a difference when I change the value.

Ok, I even took the incline out of the box that they’re walking around on, and I still end up with at least one of these guys falling through it. This leads me to believe that it just has to do with the way I’m implementing gravity to KEEP these guys on the ground.

I need some sort of gravity because they are going to be walking up and down a slope, and I need them to be able to walk down at an angle and not just walk off into mid air. If I can get the gravity working properly with them on a flat surface, then I can go back to trying the inclined one.

This brings me back to the skin width. I started making drastic changes and could see the way the characters were reacting to the floor. I tweaked it to the right value and re-added the inclined floor, and it seems to be working alright now. I added scaling back in and they’re not falling through, though every once and a while they kind of do the moonwalk because they have stopped being grounded (due to a scaling change, until the gravity value brings them back down). I’m also noticing a lot more jumpiness in terms of direction the characters are facing, but I’m pretty sure that has to do with my calculations and distance from the virtual object they are following.

I think right now this is a good starting point for a simulated projection depth effect using an orthographic camera.

I had an idea that I could use raycasting to the ground, determine the contact point of collision and set the height from that.

This would be perfect for your needs.