Skip to content

Instantly share code, notes, and snippets.

@NickDiMucci
Created March 2, 2018 14:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NickDiMucci/552f7737d1565906bff4eefd700edf4e to your computer and use it in GitHub Desktop.
Save NickDiMucci/552f7737d1565906bff4eefd700edf4e to your computer and use it in GitHub Desktop.
The player movement and collision detection code for Demons with Shotguns.
namespace com.mindshaft.overtime.controller {
public class BasicEntityController : IEntityController {
private float gravity = 40f;
private float targetSpeed = 0f;
private IMovementModel model;
[Inject]
public IEntityCollisionDetection CollisionDetection { get; set; }
public IMovementModel Move(IMovementModel model) {
// Need to reset gravity everytime in case we
// reuse an object that was flying, but is now grounded.
// gravity would then be -1.
this.model = model;
targetSpeed = (model.HorizontalInput + model.VerticalInput) * model.Speed;
model.CurrentSpeed = accelerateSpeed(model.CurrentSpeed, targetSpeed,
model.Accel);
if (model.IsJumping) {
model.AmountToMove = new Vector3(model.AmountToMove.x,
model.AmountToMove.y);
} else if (CollisionDetection.OnGround) {
model.AmountToMove = new Vector3(model.AmountToMove.x, 0);
}
model.FlipAnim = flipAnimation(targetSpeed);
// If we're ignoring gravity, then just use the vertical input.
// if it's 0, then we'll just float.
gravity = model.IgnoreGravity ? model.VerticalInput : 40f;
model.AmountToMove = new Vector3(model.CurrentSpeed, model.AmountToMove.y - gravity * Time.deltaTime);
model.FinalTransform =
CollisionDetection.Move(model.AmountToMove * Time.deltaTime,
model.BoxCollider.gameObject, model.IgnorePlayerLayer);
// Prevent the entity from moving too fast on the y-axis.
model.FinalTransform = new Vector3(model.FinalTransform.x,
Mathf.Clamp(model.FinalTransform.y, -1.0f, 1.0f),
model.FinalTransform.z);
return model;
}
private float accelerateSpeed(float currSpeed, float target, float accel) {
if (currSpeed == target) {
return currSpeed;
}
// Must currSpeed be increased or decreased to get closer to target
float dir = Mathf.Sign(target - currSpeed);
currSpeed += accel * Time.deltaTime * dir;
// If currSpeed has now passed Target then return Target, otherwise return currSpeed
return (dir == Mathf.Sign(target - currSpeed)) ? currSpeed : target;
}
private bool flipAnimation(float currSpeed) {
bool flip = model.FlipAnim;
if (currSpeed < 0) {
flip = true;
} else if (currSpeed > 0) {
flip = false;
}
return flip;
}
}
}
namespace com.mindshaft.overtime.physics {
public class RaycastCollisionDetection : IEntityCollisionDetection {
private BoxCollider _collider;
private Rect _collisionRect;
private LayerMask _collisionMask;
private LayerMask _playerMask;
public GameObject ObjectCollidedWith { get; set; }
public bool OnGround { get; set; }
public bool SideCollision { get; set; }
public bool PlayerCollisionX { get; set; }
public bool PlayerCollisionY { get; set; }
public Vector3 HitNormal { get; set; }
public void Init(GameObject entityGo) {
_collisionMask = LayerMask.NameToLayer("Collisions");
_playerMask = LayerMask.NameToLayer("Player");
_collider = entityGo.GetComponent<BoxCollider>();
}
public Vector3 Move(Vector3 moveAmount, GameObject entityGo, bool ignorePlayerLayer) {
float deltaX = moveAmount.x;
float deltaY = moveAmount.y;
Vector3 entityPosition = entityGo.transform.position;
if (deltaY != 0) {
// Resolve any possible collisions below and above the entity.
deltaY = YAxisCollisions(deltaY, Mathf.Sign(deltaX), entityPosition);
}
// Resolve any possible collisions left and right of the entity.
// Check if our deltaX value is 0 to avoid unnecessary collision detection.
if (deltaX != 0) {
deltaX = XAxisCollisions(deltaX, entityPosition, ignorePlayerLayer);
}
if (deltaX != 0 && deltaY != 0 && !SideCollision && !OnGround) {
DiagonalCollisions(ref deltaX, ref deltaY, entityPosition);
}
Vector3 finalTransform = new Vector3(deltaX, deltaY, 0);
return finalTransform;
}
private float XAxisCollisions(float deltaX, Vector3 entityPosition, bool ignorePlayerLayer) {
SideCollision = false;
PlayerCollisionX = false;
ObjectCollidedWith = null;
// It's VERY important that the entity's collider doesn't change
// shape during the game. This will cause irregular raycast hits
// and most likely cause things to go through layers.
// Ensure sprites use a fixed collider size for all frames.
_collisionRect = GetNewCollisionRect();
// Increase this value if you want the rays to start and end
// outside of the entity's collider bounds.
float margin = 0.04f;
int numOfRays = 4;
Vector3 rayStartPoint = new Vector3(_collisionRect.center.x,
_collisionRect.yMin + margin, entityPosition.z);
Vector3 rayEndPoint = new Vector3(_collisionRect.center.x,
_collisionRect.yMax - margin, entityPosition.z);
float distance = (_collisionRect.width / 2) + Mathf.Abs(deltaX);
for (int i = 0; i < numOfRays; ++i) {
float lerpAmount = (float) i / ((float) numOfRays - 1);
Vector3 origin = Vector3.Lerp(rayStartPoint, rayEndPoint, lerpAmount);
Ray ray = new Ray(origin, new Vector2(Mathf.Sign(deltaX), 0));
Debug.DrawRay(ray.origin, ray.direction, Color.white);
RaycastHit hit;
// Bit shift the layers to tell Unity to NOT ignore them.
if (Physics.Raycast(ray, out hit, distance, 1 << _collisionMask) ||
Physics.Raycast(ray, out hit, distance, 1 << _playerMask)) {
HitNormal = hit.normal;
Debug.DrawRay(ray.origin, ray.direction, Color.yellow);
float x = Mathf.Sign(deltaX) == -1
? _collisionRect.xMin
: _collisionRect.xMax;
// Give a small amount of skin space to prevent snagging.
float skinSpace = 0.005f;
deltaX = (_collisionRect.center.x + hit.distance * ray.direction.x - x) + skinSpace;
if (hit.transform.gameObject.layer == _playerMask) {
PlayerCollisionX = true;
if (!ignorePlayerLayer) {
deltaX = 0;
}
ObjectCollidedWith = hit.transform.gameObject;
} else {
SideCollision = true;
}
break;
}
}
return deltaX;
}
private float YAxisCollisions(float deltaY, float dirX, Vector3 entityPosition) {
OnGround = false;
PlayerCollisionY = false;
ObjectCollidedWith = null;
// It's VERY important that the entity's collider doesn't change
// shape during the game. This will cause irregular raycast hits
// and most likely cause things to go through layers.
// Ensure sprites use a fixed collider size for all frames.
_collisionRect = GetNewCollisionRect();
// Increase this value if you want the rays to start and end
// outside of the entity's collider bounds.
float margin = 0.04f;
int numOfRays = 4;
Vector3 rayStartPoint = new Vector3(_collisionRect.xMin + margin,
_collisionRect.center.y, entityPosition.z);
Vector3 rayEndPoint = new Vector3(_collisionRect.xMax - margin,
_collisionRect.center.y, entityPosition.z);
float distance = (_collisionRect.height / 2) + Mathf.Abs(deltaY);
for (int i = 0; i < numOfRays; ++i) {
float lerpAmount = (float) i / ((float) numOfRays - 1);
// If we are facing left, start the rays on the left side,
// else start the ray rays on the right side.
// This will help ensure precise castings on the corners.
Vector3 origin = dirX == -1
? Vector3.Lerp(rayStartPoint, rayEndPoint, lerpAmount)
: Vector3.Lerp(rayEndPoint, rayStartPoint, lerpAmount);
Ray ray = new Ray(origin, new Vector2(0, Mathf.Sign(deltaY)));
//Debug.DrawRay(ray.origin, ray.direction, Color.white);
RaycastHit hit;
// Bit shift the layers to tell Unity to NOT ignore them.
if (Physics.Raycast(ray, out hit, distance, 1 << _collisionMask) ||
Physics.Raycast(ray, out hit, distance, 1 << _playerMask)) {
HitNormal = hit.normal;
//Debug.DrawRay(ray.origin, ray.direction, Color.yellow);
float y = Mathf.Sign(deltaY) == -1
? _collisionRect.yMin
: _collisionRect.yMax;
// Give a small amount of skin space to prevent snagging.
float skinSpace = 0.0005f;
deltaY = (_collisionRect.center.y + hit.distance * ray.direction.y - y) + skinSpace;
// Only flag player collision if we collide with them while traveling down.
if (hit.collider.gameObject.layer == _playerMask && Mathf.Sign(deltaY) == -1) {
PlayerCollisionY = true;
ObjectCollidedWith = hit.transform.gameObject;
}
OnGround = true;
break;
}
}
return deltaY;
}
private void DiagonalCollisions(ref float deltaX, ref float deltaY, Vector3 entityPosition) {
_collisionRect = GetNewCollisionRect();
float distance = (_collisionRect.height / 2) + Mathf.Abs(deltaX);
Ray ray = new Ray(_collisionRect.center, new Vector2(Mathf.Sign(deltaX), Mathf.Sign(deltaY)));
Debug.DrawRay(ray.origin, ray.direction, Color.white);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, distance, 1 << _collisionMask)) {
HitNormal = hit.normal;
Debug.DrawRay(ray.origin, ray.direction, Color.yellow);
// Stop deltaX and let entity drop by deltaY.
deltaX = 0;
SideCollision = true;
}
}
private Rect GetNewCollisionRect() {
return new Rect(
_collider.bounds.min.x,
_collider.bounds.min.y,
_collider.bounds.size.x,
_collider.bounds.size.y);
}
}
}
namespace com.mindshaft.overtime.fsm {
public class StandingState : IState {
private enum StandingAnimations {
Idle,
Run,
IdleDown,
IdleUp,
RunDown,
RunUp
}
private static string[] _animToPlay = new string[] { "idle", "run", "idleDown", "idleUp", "runDown", "runUp" };
private static SoundFx[] _jumpLandings = new SoundFx[] { SoundFx.JUMP_LANDING, SoundFx.JUMP_LANDING2, SoundFx.JUMP_LANDING3, SoundFx.JUMP_LANDING4, SoundFx.JUMP_LANDING5 };
private float _time = 0.2f;
private float _timer;
private float _fallingTime;
private bool _wasFalling = false;
public void Entry(PlayerMediator mediator) {
_timer = _time;
_fallingTime = 0;
}
public void Exit(PlayerMediator mediator, IState state) {
mediator.ChangeState(state);
}
public void FixedUpdate(PlayerMediator mediator) {
}
public void HandleInput(PlayerMediator mediator) {
if (mediator.InputController.Jump() &&
mediator.EntityController.CollisionDetection.OnGround) {
Exit(mediator, new JumpingState());
}
if (mediator.View.IsTeleporting) {
Exit(mediator, new TeleportState());
}
if (mediator.InputController.Taunt() && !mediator.View.IsPossessed) {
Exit(mediator, new TauntingState());
}
if (mediator.InputController.MeleeAttackReleased()) {
if (mediator.View.ShieldView.ShieldHealth > 0
&& mediator.View.MeleeCooldownTimer < 0 && !mediator.View.IsPossessed) {
Exit(mediator, new MeleeState());
} else {
mediator.PlaySoundFxSignal.Dispatch(new PlaySoundFxModel(SoundFx.NEGATIVE, mediator.transform.position, SpatialBlend.ThreeDee));
mediator.DisplayAlertOverPlayerSignal.Dispatch(mediator.gameObject);
}
}
if (mediator.InputController.MeleeAttackPressed() && mediator.View.ShieldView.ShieldHealth > 0
&& mediator.View.MeleeCooldownTimer < 0 && !mediator.View.IsPossessed) {
mediator.View.Charge += 0.05f;
} else {
if (mediator.InputController.Shield() && mediator.View.ShieldView.ShieldHealth > 0 && !mediator.View.IsPossessed) {
Exit(mediator, new ShieldedState());
}
if (mediator.InputController.ThrowGrenade() && !mediator.View.IsPossessed) {
mediator.ActivateGrenade();
}
if (mediator.InputController.Dash()) {
if (mediator.View.DashCooldownTimer < 0 && !mediator.View.IsPossessed) {
Exit(mediator, new DashingState());
} else {
mediator.PlaySoundFxSignal.Dispatch(new PlaySoundFxModel(SoundFx.NEGATIVE, mediator.transform.position, SpatialBlend.ThreeDee));
mediator.DisplayAlertOverPlayerSignal.Dispatch(mediator.gameObject);
}
}
if (mediator.View.HasSoul && mediator.InputController.OtherAction() && !mediator.View.IsPossessed) {
mediator.ThrowSoulFlag();
}
}
}
public void OnCollisionEnter(PlayerMediator mediator, Collider other) {
}
public void Update(PlayerMediator mediator) {
_timer -= Time.deltaTime;
// Keep track if the player is falling (if he's not OnGround, then obviously he is falling).
if (!mediator.EntityController.CollisionDetection.OnGround) {
_wasFalling = true;
_fallingTime += Time.deltaTime;
}
// Only play a landing dust anim if the player was previously falling last frame, but is now OnGround
// and was falling for longer than a specified time.
if (mediator.EntityController.CollisionDetection.OnGround && _wasFalling && _fallingTime >= 0.4f) {
_wasFalling = false;
_fallingTime = 0;
mediator.PlaySoundFxSignal.Dispatch(new PlaySoundFxModel(_jumpLandings[Random.Range(0, 5)], mediator.transform.position, SpatialBlend.ThreeDee));
}
StandingAnimations animToPlay = StandingAnimations.Idle;
if (mediator.AmountToMove.x != 0.0f) {
animToPlay = StandingAnimations.Run;
// Give some time between dust anim dispatches.
if (_timer < 0 && mediator.EntityController.CollisionDetection.OnGround) {
mediator.PlayMovementDustAnimSignal.Dispatch(mediator.transform.position - new Vector3(0, 0.7f, 0), mediator.View.Sprite.FlipX, command.MovementDustType.STEPPING);
_timer = _time;
}
if (mediator.View.VerticalInput == -1) {
animToPlay = StandingAnimations.RunDown;
} else if (mediator.View.VerticalInput == 1) {
animToPlay = StandingAnimations.RunUp;
}
} else if (Mathf.Sign(mediator.View.VerticalInput) == -1 && mediator.View.VerticalInput != 0f) {
animToPlay = StandingAnimations.IdleDown;
} else if (Mathf.Sign(mediator.View.VerticalInput) == 1 && mediator.View.VerticalInput != 0f) {
animToPlay = StandingAnimations.IdleUp;
}
mediator.PlayAnim(_animToPlay[(int) animToPlay]);
if (mediator.View.Health < 1) {
Exit(mediator, new DeadState());
}
if (mediator.View.IsGrounded) {
Exit(mediator, new GroundedState());
}
}
public void LateUpdate(PlayerMediator mediator) {
if (mediator.View.IsDrunk) {
mediator.View.HorizontalInput *= -0.5f;
mediator.View.VerticalInput *= -0.5f;
}
mediator.OnInput();
}
}
}
namespace com.mindshaft.overtime.fsm {
public class JumpingState : IState {
private static string[] _animToPlay = new string[] { "jump", "fall", "jumpDown", "jumpUp" };
private static SoundFx[] _jumpLandings = new SoundFx[] { SoundFx.JUMP_LANDING, SoundFx.JUMP_LANDING2, SoundFx.JUMP_LANDING3, SoundFx.JUMP_LANDING4, SoundFx.JUMP_LANDING5 };
private float _jumpingHeight = 8f;
private bool _canDoubleJump = true;
private bool _canJump = true;
public void Entry(PlayerMediator mediator) {
mediator.RecordGameStatSignal.Dispatch(GameStatType.JUMP, mediator.View.PlayerIndex);
PlayAudio(mediator);
mediator.IsJumping = true;
mediator.PlayMovementDustAnimSignal.Dispatch(mediator.transform.position - new Vector3(0, 0.3f, 0), mediator.View.Sprite.FlipX, command.MovementDustType.JUMPING);
PlayWingsAnim(mediator);
}
private static void PlayWingsAnim(PlayerMediator mediator) {
mediator.View.AngelWingsSpriteAnim.StopAndResetFrame();
mediator.View.AngelWingsSpriteAnim.Play();
mediator.View.DevilWingsSpriteAnim.StopAndResetFrame();
mediator.View.DevilWingsSpriteAnim.Play();
}
public void Exit(PlayerMediator mediator, IState state) {
mediator.IsJumping = false;
mediator.ChangeState(state);
}
public void OnCollisionEnter(PlayerMediator mediator, Collider other) {
}
public void FixedUpdate(PlayerMediator mediator) {
}
public void HandleInput(PlayerMediator mediator) {
if (mediator.EntityController.CollisionDetection.OnGround) {
// Only dispatch a movement dust anim if the player is landing downard.
if (mediator.AmountToMove.y <= 0) {
mediator.PlayMovementDustAnimSignal.Dispatch(mediator.transform.position - new Vector3(0, 0.2f, 0), mediator.View.Sprite.FlipX, MovementDustType.LANDING);
mediator.PlaySoundFxSignal.Dispatch(new PlaySoundFxModel(_jumpLandings[Random.Range(0, 5)], mediator.transform.position, SpatialBlend.ThreeDee));
}
Exit(mediator, new StandingState());
}
if (mediator.View.IsTeleporting) {
Exit(mediator, new TeleportState());
}
// Once the player lets go of the jump button, they shouldn't be jumping anymore.
if (!mediator.InputController.Jump()) {
_canJump = false;
}
// This seems gnarly, but what we're checking is if the player has hit the jump button again since letting it
// go while in this state. We also check to see if the player isn't on a descend, which we don't want to allow
// a double jump then.
if (mediator.InputController.Jump() && !_canJump && _canDoubleJump && mediator.AmountToMove.y >= -5f) {
mediator.AmountToMove = new Vector3(mediator.AmountToMove.x, mediator.View.JumpHeight);
mediator.PlayMovementDustAnimSignal.Dispatch(mediator.transform.position - new Vector3(0, 0.3f, 0), mediator.View.Sprite.FlipX, MovementDustType.JUMPING);
PlayAudio(mediator);
PlayWingsAnim(mediator);
_canDoubleJump = false;
}
if (mediator.InputController.MeleeAttackReleased()) {
if (mediator.View.ShieldView.ShieldHealth > 0
&& mediator.View.MeleeCooldownTimer < 0 && !mediator.View.IsPossessed) {
Exit(mediator, new MeleeState());
} else {
mediator.PlaySoundFxSignal.Dispatch(new PlaySoundFxModel(SoundFx.NEGATIVE, mediator.transform.position, SpatialBlend.ThreeDee));
mediator.DisplayAlertOverPlayerSignal.Dispatch(mediator.gameObject);
}
}
if (mediator.InputController.MeleeAttackPressed() && mediator.View.ShieldView.ShieldHealth > 0
&& mediator.View.MeleeCooldownTimer < 0 && !mediator.View.IsPossessed) {
mediator.View.Charge += 0.05f;
} else {
if (mediator.InputController.Shield() && mediator.View.ShieldView.ShieldHealth > 0 && !mediator.View.IsPossessed) {
Exit(mediator, new ShieldedState());
}
if (mediator.InputController.ThrowGrenade() && !mediator.View.IsPossessed) {
mediator.ActivateGrenade();
}
if (mediator.InputController.Dash()) {
if (mediator.View.DashCooldownTimer < 0 && !mediator.View.IsPossessed) {
Exit(mediator, new DashingState());
} else {
mediator.PlaySoundFxSignal.Dispatch(new PlaySoundFxModel(SoundFx.NEGATIVE, mediator.transform.position, SpatialBlend.ThreeDee));
mediator.DisplayAlertOverPlayerSignal.Dispatch(mediator.gameObject);
}
}
if (mediator.View.HasSoul && mediator.InputController.OtherAction() && !mediator.View.IsPossessed) {
mediator.ThrowSoulFlag();
}
}
}
public void Update(PlayerMediator mediator) {
if (_canJump) {
if (_jumpingHeight < mediator.View.JumpHeight) {
_jumpingHeight = Mathf.Lerp(_jumpingHeight, _jumpingHeight += 20, 2 * Time.deltaTime);
mediator.AmountToMove = new Vector3(mediator.AmountToMove.x, mediator.View.JumpHeight);
}
}
int animToPlay = mediator.AmountToMove.y > 0 ? 0 : 1;
if (mediator.View.VerticalInput == -1) {
animToPlay = 2;
} else if (mediator.View.VerticalInput == 1) {
animToPlay = 3;
}
mediator.PlayAnim(_animToPlay[animToPlay]);
if (mediator.View.Health < 1) {
Exit(mediator, new DeadState());
}
if (mediator.View.IsGrounded) {
Exit(mediator, new GroundedState());
}
}
public void LateUpdate(PlayerMediator mediator) {
if (mediator.View.IsDrunk) {
mediator.View.HorizontalInput *= 0.5f;
mediator.View.VerticalInput *= 0.5f;
}
mediator.OnInput();
}
private void PlayAudio(PlayerMediator mediator) {
mediator.PlayJumpClip();
}
}
}
namespace com.mindshaft.overtime.view {
public class PlayerMediator : Mediator {
// OnInput should be called from within the FSM
// in order to have fine control over when
// the player can move and how.
public void OnInput() {
_movement.HorizontalInput = View.HorizontalInput;
_movement.CurrentSpeed = CurrentSpeed;
_movement.Speed = View.Speed;
_movement.Accel = View.Acceleration;
_movement.IsJumping = IsJumping;
_movement.FlipAnim = _flipAnim;
_movement.AmountToMove = AmountToMove;
_movement.Position = transform.position;
_movement.BoxCollider = View.BoxCollider;
_movement = EntityController.Move(_movement);
OnMovementCalculated(_movement);
}
private void OnMovementCalculated(IMovementModel calculatedMovement) {
CurrentSpeed = calculatedMovement.CurrentSpeed;
View.CurrentSpeed = CurrentSpeed;
AmountToMove = calculatedMovement.AmountToMove;
View.AmountToMove = AmountToMove;
_flipAnim = calculatedMovement.FlipAnim;
// If player is grounded, let them fall but don't let them move along the X axis.
// This will allow us to properly flip sprites if they move left or right.
View.FinalTransform = View.IsGrounded ? new Vector3(0, calculatedMovement.FinalTransform.y, calculatedMovement.FinalTransform.z) : calculatedMovement.FinalTransform;
// To allow strafing, check if the player is holding down the shoot button
// if so, don't flip the sprite.
if (!InputController.ShootDown()) {
View.FlipSprite(calculatedMovement.FlipAnim);
}
transform.Translate(View.FinalTransform);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment