/// <summary> /// This function is called every fixed framerate frame, if the MonoBehaviour is enabled. /// </summary> void FixedUpdate() { // For convenience box = new Rect( cd.bounds.min.x, cd.bounds.min.y, cd.bounds.size.x, cd.bounds.size.y ); // Set default values finalJumpSpeed = jumpSpeed; finalAccel = accel; // --- Input --- // TODO: Put this in update? // Get input and set idle state if applicable float hAxis = 0; bool dashInput = false; bool jumpInput = false; // Restrict input if player is hit or attacking with restricted movement if (state.condState.Missing(PlayerState.Condition.Hit) && state.condState.Missing(PlayerState.Condition.RestrictedAttacking)) { hAxis = Input.GetAxisRaw("Horizontal"); dashInput = Input.GetButtonDown("Fire1"); jumpInput = Input.GetButton("Jump"); } // Set facing direction if (hAxis != 0) { facing = new Vector2(hAxis, 0); } // Set idle state according to input if (hAxis == 0f && !dashInput && !jumpInput) { state.moveState = state.moveState.Include(PlayerState.Movement.Idle); } else { state.moveState = state.moveState.Remove(PlayerState.Movement.Idle); } // --- Gravity & Ground Check --- // Set flag to prevent player from jumping before character lands state.moveState = state.moveState.Remove(PlayerState.Movement.Landing); // If player is not grounded or sticking to wall, apply gravity if (!grounded && state.moveState.Missing(PlayerState.Movement.WallSticking) && state.moveState.Missing(PlayerState.Movement.WallSliding)) { velocity = new Vector2(velocity.x, Mathf.Max(velocity.y - gravity, -maxFall)); } // Check if player is currently falling if (velocity.y < 0) { if (state.moveState.Missing(PlayerState.Movement.Falling)) { state.moveState = state.moveState.Include(PlayerState.Movement.Falling); if (OnFall != null) { OnFall(this, System.EventArgs.Empty); } } } // Check for collisions below // (No need to check if player is in mid-air but not falling) if (grounded || state.moveState.Has(PlayerState.Movement.Falling)) { // Determine first and last rays Vector2 minRay = new Vector2(box.xMin + margin, box.center.y); Vector2 maxRay = new Vector2(box.xMax - margin, box.center.y); // Calculate ray distance (if not grounded, set to current fall speed) float rayDistance = box.height / 2 + ((grounded) ? margin : Mathf.Abs(velocity.y * Time.deltaTime)); // Check below for ground RaycastHit2D[] hitInfo = new RaycastHit2D[vRays]; bool hit = false; float closestHit = float.MaxValue; int closestHitIndex = 0; for (int i = 0; i < vRays; i++) { // Create and cast ray float lerpDistance = (float)i / (float)(vRays - 1); Vector2 rayOrigin = Vector2.Lerp(minRay, maxRay, lerpDistance); Ray2D ray = new Ray2D(rayOrigin, Vector2.down); hitInfo[i] = Physics2D.Raycast(rayOrigin, Vector2.down, rayDistance, RayLayers.downRay); // Check raycast results and keep track of closest ground hit if (hitInfo[i].fraction > 0) { hit = true; if (hitInfo[i].fraction < closestHit) { closestHit = hitInfo[i].fraction; closestHitIndex = i; closestHitInfo = hitInfo[i]; } } } // If player hits ground, snap to the closest ground if (hit) { // Check if player is landing this frame if (state.moveState.Has(PlayerState.Movement.Falling) && state.moveState.Missing(PlayerState.Movement.Landing)) { if (OnLand != null) { OnLand(this, System.EventArgs.Empty); } state.moveState = state.moveState.Include(PlayerState.Movement.Landing); state.moveState = state.moveState.Remove(PlayerState.Movement.Jumping); } grounded = true; state.moveState = state.moveState.Remove(PlayerState.Movement.Falling); state.moveState = state.moveState.Remove(PlayerState.Movement.WallSticking | PlayerState.Movement.WallSliding); exitingDash = false; Debug.DrawLine(box.center, hitInfo[closestHitIndex].point, Color.white, 1f); transform.Translate(Vector2.down * (hitInfo[closestHitIndex].distance - box.height / 2)); velocity = new Vector2(velocity.x, 0); // Check if player is on a moving platform MovingPlatform newMovingPlatform = hitInfo[closestHitIndex].transform.parent.gameObject.GetComponent <MovingPlatform>(); if (newMovingPlatform != null) { movingPlatform = newMovingPlatform; movingPlatform.GetOnPlatform(gameObject); } // Check ground for special attributes groundTypes = hitInfo[closestHitIndex].collider.gameObject.GetComponents <SpecialGround>(); } else { grounded = false; // Clear ground properties groundTypes = Enumerable.Empty <SpecialGround>(); if (movingPlatform != null) { movingPlatform.GetOffPlatform(gameObject); movingPlatform = null; } } } if (state.moveState.Has(PlayerState.Movement.Landing)) { state.moveState = state.moveState.Remove(PlayerState.Movement.Dashing); exitingDash = false; } // --- Lateral Movement & Collisions --- // Get input float newVelocityX = velocity.x; ApplyGroundEffects(); // Move if input exists if (hAxis != 0) { newVelocityX += finalAccel * hAxis; // Clamp speed to max if not exiting a dash (in order to keep air momentum) newVelocityX = Mathf.Clamp(newVelocityX, -maxSpeed, maxSpeed); // Dash if (canDash) { if (dashInput) { if (grounded && dashReady) { StartCoroutine(Dash()); newVelocityX = dashSpeed * hAxis; } else if ((state.moveState.Has(PlayerState.Movement.WallSticking) || state.moveState.Has(PlayerState.Movement.WallSliding)) & dashReady) { StartCoroutine(Dash()); //newVelocityX = dashSpeed * hAxis; } } } // Account for slope if (Mathf.Abs(closestHitInfo.normal.x) > 0.1f) { float friction = 0.7f; newVelocityX = Mathf.Clamp((newVelocityX - (closestHitInfo.normal.x * friction)), -maxSpeed, maxSpeed); Vector2 newPosition = transform.position; newPosition.y += -closestHitInfo.normal.x * Mathf.Abs(newVelocityX) * Time.deltaTime * ((newVelocityX - closestHitInfo.normal.x > 0) ? 1 : -1); transform.position = newPosition; state.moveState = state.moveState.Remove(PlayerState.Movement.Landing); } } // Decelerate if moving without input else if (velocity.x != 0) { int decelDir = (velocity.x > 0) ? -1 : 1; // Ensure player doesn't decelerate past zero newVelocityX += (velocity.x > 0) ? ((newVelocityX + finalAccel * decelDir) < 0) ? -newVelocityX : finalAccel * decelDir : ((newVelocityX + finalAccel * decelDir) > 0) ? -newVelocityX : finalAccel * decelDir; } velocity = new Vector2(newVelocityX, velocity.y); // Check for lateral collisions lateralCollision = false; // (This condition will always be true, of course. It's temporary, but allows for moving platforms to push you while not riding them.) if (velocity.x != 0 || velocity.x == 0) { // Determine first and last rays Vector2 minRay = new Vector2(box.center.x, box.yMin); Vector2 maxRay = new Vector2(box.center.x, box.yMax); // Calculate ray distance and determine direction of movement float rayDistance = box.width / 2 + Mathf.Abs(newVelocityX * Time.deltaTime); Vector2 rayDirection = (newVelocityX > 0) ? Vector2.right : Vector2.left; RaycastHit2D[] hitInfo = new RaycastHit2D[hRays]; float closestHit = float.MaxValue; int closestHitIndex = 0; float lastFraction = 0; int numHits = 0; // for debugging for (int i = 0; i < hRays; i++) { // Create and cast ray float lerpDistance = (float)i / (float)(hRays - 1); Vector2 rayOrigin = Vector2.Lerp(minRay, maxRay, lerpDistance); hitInfo[i] = Physics2D.Raycast(rayOrigin, rayDirection, rayDistance, RayLayers.sideRay); Debug.DrawRay(rayOrigin, rayDirection * rayDistance, Color.cyan, Time.deltaTime); // Check raycast results if (hitInfo[i].fraction > 0) { lateralCollision = true; numHits++; // for debugging if (hitInfo[i].fraction < closestHit) { closestHit = hitInfo[i].fraction; closestHitIndex = i; } // If more than one ray hits, check the slope of what player is colliding with if (lastFraction > 0) { float slopeAngle = Vector2.Angle(hitInfo[i].point - hitInfo[i - 1].point, Vector2.right); //Debug.Log(Mathf.Abs(slopeAngle)); // for debugging // If we hit a wall, snap to it if (Mathf.Abs(slopeAngle - 90) < angleLeeway) { transform.Translate(rayDirection * (hitInfo[i].distance - box.width / 2)); if (OnLateralCollision != null) { OnLateralCollision(this, System.EventArgs.Empty); } velocity = new Vector2(0, velocity.y); // Wall sticking if (canStickWall && !grounded && (state.moveState.Missing(PlayerState.Movement.WallSticking) && state.moveState.Missing(PlayerState.Movement.WallSliding))) { // Only stick if moving towards wall if (hAxis != 0 && ((hAxis < 0) == (rayDirection.x < 0))) { state.moveState = state.moveState.Include(PlayerState.Movement.WallSticking); state.moveState = state.moveState.Remove(PlayerState.Movement.Jumping); wallDirection = rayDirection; velocity = new Vector2(0, 0); wallSlideTime = Time.time + wallSlideDelay; } } break; } } lastFraction = hitInfo[i].fraction; } } // Wall sticking if (state.moveState.Has(PlayerState.Movement.WallSticking) || state.moveState.Has(PlayerState.Movement.WallSliding)) { velocity = new Vector2(0, velocity.y); bool onWall = false; float lowestY = float.MaxValue; RaycastHit2D lowestYHit = new RaycastHit2D(); // Check for wall regardless of horizontal velocity (allows player to hold direction away from wall) for (int i = 0; i < hRays; i++) { // Create and cast ray float lerpDistance = (float)i / (float)(hRays - 1); Vector2 rayOrigin = Vector2.Lerp(minRay, maxRay, lerpDistance); hitInfo[i] = Physics2D.Raycast(rayOrigin, wallDirection, (box.width / 2) + .001f, RayLayers.sideRay); if (hitInfo[i].fraction > 0) { onWall = true; if (hitInfo[i].point.y < lowestY) { lowestY = hitInfo[i].point.y; lowestYHit = hitInfo[i]; } } } // If hitting wall while sliding, update wall slide PS position if (onWall && state.moveState.Has(PlayerState.Movement.WallSliding)) { if (currentWallSlidePS == null) { // TODO: Create a new PS if the old one hasn't disappeared yet currentWallSlidePS = Instantiate(wallSlidePS, new Vector3(lowestYHit.point.x, lowestYHit.point.y, 0f), Quaternion.identity); } else { currentWallSlidePS.transform.position = new Vector3(lowestYHit.point.x, lowestYHit.point.y, 0f); ParticleSystem ps = currentWallSlidePS.GetComponentInChildren <ParticleSystem>(); ps.Emit(1); lastWallSlidePSEmission = Time.time; } } // If no wall hit, end wallstick/slide if (!onWall) { state.moveState = state.moveState.Remove(PlayerState.Movement.WallSticking | PlayerState.Movement.WallSliding); } } // If not wall sliding, remove PS else if (currentWallSlidePS != null) { Destroy(currentWallSlidePS, lastWallSlidePSEmission * .75f); } // Wall sliding if (state.moveState.Has(PlayerState.Movement.WallSticking) && Time.time >= wallSlideTime) { velocity = Vector2.down * wallSlideSpeed; state.moveState = state.moveState.Remove(PlayerState.Movement.WallSticking); state.moveState = state.moveState.Include(PlayerState.Movement.WallSliding); } } // --- Jumping --- if (canJump && state.moveState.Missing(PlayerState.Movement.Landing)) { // Prevent player from holding down jump to autobounce if (jumpInput && !jumpPressedLastFrame) { prevJumpDownTime = Time.time; } else if (!jumpInput) { prevJumpDownTime = 0f; } if (Time.time - prevJumpDownTime < jumpPressLeeway) { // Normal jump if (grounded) { velocity = new Vector2(velocity.x, finalJumpSpeed); prevJumpDownTime = 0f; canDoubleJump = true; } // Wall jump else if (state.moveState.Has(PlayerState.Movement.WallSticking) || state.moveState.Has(PlayerState.Movement.WallSliding)) { velocity = new Vector2(-wallDirection.x * wallJumpAwayDistance, finalJumpSpeed * .8f); state.moveState = state.moveState.Remove(PlayerState.Movement.WallSticking | PlayerState.Movement.WallSliding); canDoubleJump = false; } // Double jump else if (!grounded && dashReady && canDoubleJump) { velocity = new Vector2(velocity.x, finalJumpSpeed * .75f); Instantiate(doubleJumpPS, new Vector2(box.center.x, box.center.y - box.height / 2), Quaternion.identity); prevJumpDownTime = 0f; canDoubleJump = false; } state.moveState = state.moveState.Remove(PlayerState.Movement.Falling); state.moveState = state.moveState.Include(PlayerState.Movement.Jumping); } jumpPressedLastFrame = jumpInput; } // --- Ceiling Check --- // Only check if we're grounded or jumping if (grounded || velocity.y > 0) { // Determine first and last rays Vector2 minRay = new Vector2(box.xMin + margin, box.center.y); Vector2 maxRay = new Vector2(box.xMax - margin, box.center.y); // Calculate ray distance (if not grounded, set to current jump speed) float rayDistance = box.height / 2 + ((grounded) ? margin : velocity.y * Time.deltaTime); // Check above for ceiling RaycastHit2D[] hitInfo = new RaycastHit2D[vRays]; bool hit = false; float closestHit = float.MaxValue; int closestHitIndex = 0; for (int i = 0; i < vRays; i++) { // Create and cast ray float lerpDistance = (float)i / (float)(vRays - 1); Vector2 rayOrigin = Vector2.Lerp(minRay, maxRay, lerpDistance); Ray2D ray = new Ray2D(rayOrigin, Vector2.up); hitInfo[i] = Physics2D.Raycast(rayOrigin, Vector2.up, rayDistance, RayLayers.upRay); // Check raycast results and keep track of closest ceiling hit if (hitInfo[i].fraction > 0) { hit = true; if (hitInfo[i].fraction < closestHit) { closestHit = hitInfo[i].fraction; closestHitIndex = i; } } } // If we hit ceiling, snap to the closest ceiling // TODO: Maybe give rebound instead of snapping? if (hit) { transform.Translate(Vector3.up * (hitInfo[closestHitIndex].distance - box.height / 2)); if (OnCeilingCollision != null) { OnCeilingCollision(this, System.EventArgs.Empty); } velocity = new Vector2(velocity.x, 0); } } // --- Damage --- // Apply hit if detected by collider if (enemyHurtbox != null && state.condState.Missing(PlayerState.Condition.Hit) && state.condState.Missing(PlayerState.Condition.Recovering)) { state.condState = state.condState.Include(PlayerState.Condition.Hit); state.condState = state.condState.Remove(PlayerState.Condition.Normal); state.moveState = state.moveState.Remove(PlayerState.Movement.Dashing | PlayerState.Movement.WallSticking | PlayerState.Movement.WallSliding); velocity = Vector2.zero; StartCoroutine(ApplyHit(enemyHurtbox.damage, enemyHurtbox.knockback, enemyHurtbox.knockbackDirection)); } //Debug.Log(state); }