EDIT - Solution found! See reply below
To help me learn Unity 5 (on Windows 7 64-bit) I am creating a clone of a platform game. The platform blocks are organised in a grid wrapped around a cylindrical tower. The player character, enemies and other level elements are constrained to a fixed radius around the tower axis. The camera is always on a horizontal line from the tower axis to the centre of the player and looks towards the axis - ie the camera follows the player around the tower with the player always at the centre of the viewport. The original game used orthographic projection - everything rendered as bitmaps - while I am working with perspective projection - tower, platforms, etc all 3D meshes.
My difficulty at the moment is in deciding how best to use Unity’s physics engine to apply the cylindrical constraint and the following gameplay rules:
– The player is always side-on to the camera ie is pointing along a tangent of the tower.
– When the player reaches an obstacle - a platform at a different height to the one he is walking on - the player automatically hops vertically. If the obstacle is not too high, the player is then moved horizontally onto the obstacle.
– The player is affected by gravity, ie falls when jumping or walking off a platform edge.
– In addition to fixed platform blocks, there are disappearing blocks (disappear when the player lands on top of them but not when hit from the sides or underneath) shootable blocks (disappear when shot by the player) and sliding blocks (cause the player to move in a particular direction when there is no player directional input).
– Horizontal enemies travel in a horizontal plane around the tower, only changing direction when they collide with a platform or another enemy. Similarly for vertical enemies.
– Rolling ball enemies travel along adjacent horizontal platforms and change direction when they collide with the side of another platform at a different height or have reached the edge of the last adjacent platform at the same height.
– Bouncing ball enemies move horizontally and are affected by gravity but always bounce to same height.
– When the player collides with an enemy, the player is sent tumbling on a vertical trajectory and eventually lands on a lower platform (if there is one). But the enemy trajectory is unaltered.
Below is what I’ve tried so far and the problems I’ve had. Any ideas on how I could do things better would be most welcome.
It seems obvious that the permanently fixed platform blocks should be static colliders (non-trigger collider, no rigid body component). This has not varied in any of my experiments. There is the issue as to what shape collider I should use (currently using a MeshCollider as the platforms are not quite close enough to being rectangular) but I won’t go into that here - this post is already too long!
Before attempting to apply the above rules, my first experiment was with rolling balls (rigid bodies) that could bounce and fall onto lower platforms. In the FixedUpdate() method I:
– scaled the XZ components of the position to a fixed distance from the tower axis
– rotated the XZ component of the velocity so that it was tangential to the tower
– set the angular velocity axis to a horizontal line from the tower axis to the ball centre
The result of this looked great and collision detection seemed to be unaffected. However I suspect using such a method might significantly affect performance if used on many rigid bodies. Further it does not really help in applying many of the rules above. And finally I get the impression that one should be setting things up to make the most of the physics engine, ie get it to do most of the work and not fight the results it generates too much.
I then tried implementing player control. Note that I am using the State design pattern with state sub-classes (Stationary, Walking, Hopping, Falling, etc) all implementing an Update() method. The current state’s Update() method is called from the script’s MonoBehaviour.Update() method.
I began with adding a RigidBody component to the player object and tried using AddForce(vel,ForceMode.VelocityChange). But it was not obvious to me what velocity change would maintain a constant distance from the tower axis, a constant walking speed, and keep the player pointing in a direction tangential to the tower. Setting the absolute velocity in FixedUpdate() might be easier to manage but, as above, this seems undesirable and anyway I believe Input is not updated until just before calls to Update(). I also found that most of my attempts at using AddForce() led to the player rolling - though now I relaise I could have used RigidBody.freezeRotation.
I thought of making the player a kinematic rigid body but this meant having to implement collision detection with platforms myself - otherwise the player passed through platforms
So that left the CharacterCollider option which has its own set of problems:
– The vertical capsule collider is not a great fit for my squat character mesh but is just about acceptable. However the rounded ends of the collider can cause the player to get stuck on the corners of platforms. In OnControllerColliderHit(hit) I had to check if hit.normal was pointing upwards and only then consider the player to be properly grounded.
– I had to set stepOffset to 0 because CharacterCollider automatic stepping is instantaneous while I need hopping onto a platform to take a non-zero time.
– In order to ensure the player does not alter enemy trajectories on collision, I had to make the enemies kinematic rigid body trigger colliders. I also had to implement OnTriggerEnter() in the player script to make the player fall. Actually making enemies kinematic makes sense, particularly for those that move horizontally and have curved trajectories around the tower.
– Obviously disappearing blocks and shootable blocks should not be static colliders. As non-kinematic rigid bodies, they properly interact with a CharacterCollider in most respects. However they do not receive trigger/collision messages and instead I had to use OnControllerColliderHit() in the player control script. (I assume this is because collision handlers are for use in the FixedUpdate()/internal physics update loop while OnControllerColliderHit deals with collisions resulting from calls to CharacterCollider.Move().)
But I don’t want the logic for interacting with these platform types to be in the player script - this should happen in the platform scripts. So what is the best way of notifying platforms of a collision with the player?
I already have all platform types implementing an ILevelElement interface (used for initialisation when loading the level or resetting the level after a life is lost). I suppose I could add a ReactToPlayer() method to this interface and use it from the player script.
hit.gameObject.GetComponent<ILevelElement>().ReactToPlayer()
But this seems an unnecessary performance hit considering most platforms are fixed and do not have any special behaviour on player contact. It would be so much better if CharacterCollider sent messages to objects it collides with.
– I need to be able to detect when the player is on the edge of platform with no adjacent platforms since in this teetering state any further directional input should cause the player to fall backwards off the platform. (This is also relevant to making rolling balls change their direction upon reaching a platform edge.)
– A severe limitation of the CharacterCollider component is the restriction to a vertical capsule collider. What if, for example, I wanted to make the player able to fight ie kick and punch etc? In that case I would have thought a compound collider would be appropriate - colliders on child game objects for fists and feet etc. But this would need a RigidBody component and this is not allowed in combination with a CharacterCollider.
All my attempts to keep the player facing a direction tangential to the tower have met with problems. For example calling my MoveFace() method (below) after calls to CharacterCollider.Move() causes some very strange behaviour. Say I’m walking to the left (angle to x-axis drops from 360 degrees) everything seems fine until at 300 degrees the player lurches by a much bigger angle change than expected. If I walk a bit further, the player is then rapidly spun around the tower to around 12 degrees.
My suspicion is that the rotation messes up collision detection somehow causing calls to Move() to behave unpredictably. But this leaves my wondering what to do to keep the player facing the correct direction…
public void MoveFace ()
{
Vector3 angles = transform.localEulerAngles;
angles.y = 360f - XAxisAngleDegrees(transform.localPosition);
transform.localEulerAngles = angles;
}
public float XAxisAngleDegrees (Vector3 v)
{
Vector2 vXZ = new Vector2(v.x, v.z);
float angle = Mathf.Acos(Mathf.Clamp(vXZ.normalized.x, -1.0f, 1.0f));
angle *= (180.0f / Mathf.PI);
if (vXZ.y < 0.0f)
{
angle = 360.0f - angle;
}
return angle;
}
/*Note that the player and camera game objects are children of
an empty game object located at the world origin. This is merely to
make organisation simpler for the split-screen multiplayer mode and
is why I refer to localPosition and localEulerAngles in the above.*/