LADXHD/InGame/GameObjects/Base/Systems/SystemBody.cs

535 lines
25 KiB
C#
Raw Normal View History

2023-12-14 22:21:22 +00:00
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using ProjectZ.Base;
using ProjectZ.InGame.GameObjects.Base.Components;
using ProjectZ.InGame.GameObjects.Base.Pools;
using ProjectZ.InGame.GameObjects.Things;
using ProjectZ.InGame.Map;
using ProjectZ.InGame.Things;
namespace ProjectZ.InGame.GameObjects.Base.Systems
{
class SystemBody
{
public ComponentPool Pool;
private readonly List<GameObject> _objectList = new List<GameObject>();
private readonly List<GameObject> _holeList = new List<GameObject>();
public void Update(int threadIndex, int threadCount)
{
if (Game1.TimeMultiplier <= 0)
return;
_objectList.Clear();
Pool.GetComponentList(_objectList,
(int)((MapManager.Camera.X - Game1.RenderWidth / 2) / MapManager.Camera.Scale),
(int)((MapManager.Camera.Y - Game1.RenderHeight / 2) / MapManager.Camera.Scale),
(int)(Game1.RenderWidth / MapManager.Camera.Scale),
(int)(Game1.RenderHeight / MapManager.Camera.Scale), BodyComponent.Mask);
foreach (var gameObject in _objectList)
{
if (!gameObject.IsActive)
continue;
var component = gameObject.Components[BodyComponent.Index] as BodyComponent;
if (component.IsActive)
UpdateBody(component);
}
}
private void UpdateBody(BodyComponent body)
{
var collisionType = Values.BodyCollision.None;
body.SpeedMultiply = 1;
body.WasGrounded = body.IsGrounded;
if (!Pool.Map.Is2dMap)
{
// z position update
if (!body.IgnoresZ)
collisionType |= UpdateVelocityZ(body);
// hole pulling
if (!body.IgnoreHoles)
UpdateHole(body);
else
body.HoleAbsorption = Vector2.Zero;
}
var velocityTargetMult = 1f;
// the speed gets limited by the velocity and the hole absorption vector
if (!body.DisableVelocityTargetMultiplier)
{
float velocityLength;
if (Pool.Map.Is2dMap && !body.CurrentFieldState.HasFlag(MapStates.FieldStates.DeepWater))
velocityLength = Math.Abs(body.Velocity.X) * 2.5f;
else
velocityLength = (new Vector2(body.Velocity.X, body.Velocity.Y).Length() + body.HoleAbsorption.Length()) * 1.5f;
velocityTargetMult = MathHelper.Clamp(1f - velocityLength, 0, 1);
}
body.DisableVelocityTargetMultiplier = false;
var velocityTarget = body.VelocityTarget * body.SpeedMultiply * velocityTargetMult;
// AdditionalMovement should slide because the raft is using it to move
var bodyOffset = velocityTarget + body.HoleAbsorption + body.AdditionalMovementVT;
var slideOffset = body.SlideOffset;
var velocityOffset = (new Vector2(body.Velocity.X, body.Velocity.Y) * (0.5f + body.SpeedMultiply * 0.5f)) * Game1.TimeMultiplier;
body.LastVelocityTarget = body.VelocityTarget;
body.LastAdditionalMovementVT = body.AdditionalMovementVT;
if (body.RestAdditionalMovement)
body.AdditionalMovementVT = Vector2.Zero;
body.SlideOffset = Vector2.Zero;
collisionType |= MoveBody(body, slideOffset + bodyOffset * Game1.TimeMultiplier, body.CollisionTypes | body.AvoidTypes,
body.IsPusher, body.IsSlider, false);
// in 2d mode the velocity is also used to push, currently used for stomping goombas
// if the player gets pushed onto a push trigger it should not get activated
collisionType |= MoveBody(body, velocityOffset, body.CollisionTypes, Pool.Map.Is2dMap && body.IsPusher, false, true);
// set IsGrounded in 2d mode
if (Pool.Map.Is2dMap)
{
body.IsGrounded = (collisionType & Values.BodyCollision.Vertical) != 0 && body.Velocity.Y > 0;
if (body.IsGrounded)
{
// bounce of the ground
if (!body.WasGrounded && body.Velocity.Y * body.Bounciness2D > 0.4f)
body.Velocity.Y = -body.Velocity.Y * body.Bounciness2D;
else
body.Velocity.Y = 0;
}
if (!body.IgnoresZ && (body.CurrentFieldState & MapStates.FieldStates.Init) == 0)
{
if (!body.IgnoresZ && (body.CurrentFieldState & MapStates.FieldStates.DeepWater) == 0)
body.Velocity.Y += body.Gravity2D * Game1.TimeMultiplier;
else if (!body.IgnoresZ && (body.CurrentFieldState & MapStates.FieldStates.DeepWater) != 0)
body.Velocity.Y += body.Gravity2DWater * Game1.TimeMultiplier;
}
}
var drag = body.IsGrounded ? body.Drag : body.DragAir;
if (body.CurrentFieldState.HasFlag(MapStates.FieldStates.DeepWater))
drag = body.DragWater;
// apply drag if the body is grounded
body.Velocity.X *= (float)Math.Pow(drag, Game1.TimeMultiplier);
if (Math.Abs(body.Velocity.X) < 0.01f * Game1.TimeMultiplier)
body.Velocity.X = 0;
if (!Pool.Map.Is2dMap || body.CurrentFieldState.HasFlag(MapStates.FieldStates.DeepWater))
{
body.Velocity.Y *= (float)Math.Pow(drag, Game1.TimeMultiplier);
if (Math.Abs(body.Velocity.Y) < 0.01f * Game1.TimeMultiplier)
body.Velocity.Y = 0;
}
if (body.Position.HasChanged())
body.Position.NotifyListeners();
// get the current field the body is on
var lastFieldState = body.CurrentFieldState;
if (body.UpdateFieldState)
body.CurrentFieldState = GetFieldState(body);
if (body.Position.Z <= 0 && body.SplashEffect && lastFieldState != MapStates.FieldStates.Init && (body.CurrentFieldState & MapStates.FieldStates.DeepWater) != 0)
{
if (body.Owner.Map.Is2dMap && (lastFieldState & MapStates.FieldStates.DeepWater) == 0)
{
body.Velocity.Y *= 0.25f;
Game1.GameManager.PlaySoundEffect("D360-14-0E");
// spawn splash animation
var splashAnimator = new ObjAnimator(body.Owner.Map, 0, 0, 0, 3, 1, "Particles/splash", "idle", true);
splashAnimator.EntityPosition.Set(new Vector2(body.Position.X, body.Position.Y - 9));
Game1.GameManager.MapManager.CurrentMap.Objects.SpawnObject(splashAnimator);
}
body.OnDeepWaterFunction?.Invoke();
}
// inform the listener of the collision
body.VelocityCollision = collisionType;
if (collisionType != Values.BodyCollision.None)
body.MoveCollision?.Invoke(collisionType);
body.LastVelocityCollision = collisionType;
}
public static MapStates.FieldStates GetFieldState(BodyComponent body)
{
var state = Game1.GameManager.MapManager.CurrentMap.GetFieldState(
new Vector2(body.BodyBox.Box.X + body.BodyBox.Box.Width / 2, body.BodyBox.Box.Front - 0.01f)) &
~(MapStates.FieldStates.Water | MapStates.FieldStates.DeepWater | MapStates.FieldStates.Lava) |
Game1.GameManager.MapManager.CurrentMap.GetFieldState(
new Vector2(body.BodyBox.Box.X + body.BodyBox.Box.Width / 2, body.BodyBox.Box.Front + body.DeepWaterOffset)) &
(MapStates.FieldStates.Water | MapStates.FieldStates.DeepWater | MapStates.FieldStates.Lava);
return state;
}
public static Values.BodyCollision MoveBody(BodyComponent body, Vector2 offset, Values.CollisionTypes collisionTypes, bool isPusher, bool slide, bool ignoreField)
{
var collisionType = Values.BodyCollision.None;
// move body in one step without aligning it to colliding objects
if (body.SimpleMovement)
{
var collidingBox = Box.Empty;
var direction = AnimationHelper.GetDirection(offset);
if (!Collision(body, body.Position.X + offset.X, body.Position.Y + offset.Y, direction, collisionTypes, ignoreField, ref collidingBox))
{
body.Position.X += offset.X;
body.Position.Y += offset.Y;
}
else
{
// the returned collision type is not as precise as with the other method
collisionType |= (direction % 2 == 0 ? Values.BodyCollision.Horizontal : Values.BodyCollision.Vertical);
if (direction == 0)
collisionType |= Values.BodyCollision.Left;
else if (direction == 1)
collisionType |= Values.BodyCollision.Top;
else if (direction == 2)
collisionType |= Values.BodyCollision.Right;
else if (direction == 3)
collisionType |= Values.BodyCollision.Bottom;
}
return collisionType;
}
if (offset.X != 0)
{
var collidingBox = Box.Empty;
// move horizontally
if (!Collision(body, body.Position.X + offset.X, body.Position.Y, offset.X < 0 ? 0 : 2, collisionTypes, ignoreField, ref collidingBox))
{
body.Position.X += offset.X;
}
else
{
var nullBox = Box.Empty;
collisionType |= offset.X < 0 ? Values.BodyCollision.Left : Values.BodyCollision.Right;
collisionType |= Values.BodyCollision.Horizontal;
// try to move around the object if there is space around it
if (slide)
{
var sliderOffset = Math.Abs(offset.X * 0.5f);
if (offset.Y >= 0 && !Collision(body, body.Position.X + offset.X,
body.Position.Y + body.MaxSlideDistance, offset.X < 0 ? 0 : 2, collisionTypes, ignoreField, ref nullBox))
{
body.SlideOffset.Y += sliderOffset;
}
else if (offset.Y <= 0 && !Collision(body, body.Position.X + offset.X,
body.Position.Y - body.MaxSlideDistance, offset.X < 0 ? 0 : 2, collisionTypes, ignoreField, ref nullBox))
{
body.SlideOffset.Y -= sliderOffset;
}
}
// align with the collided object
if (offset.X < 0 &&
Math.Abs(body.Position.X - collidingBox.Right + body.OffsetX) < Math.Abs(offset.X) &&
!Collision(body, collidingBox.Right - body.OffsetX, body.Position.Y, 0, collisionTypes, ignoreField, ref nullBox))
{
body.Position.X = collidingBox.Right - body.OffsetX;
}
else if (offset.X > 0 &&
Math.Abs(body.Position.X - (collidingBox.X - (body.Width + body.OffsetX))) < Math.Abs(offset.X) &&
!Collision(body, collidingBox.X - (body.Width + body.OffsetX), body.Position.Y, 2, collisionTypes, ignoreField, ref nullBox))
{
body.Position.X = collidingBox.X - (body.Width + body.OffsetX);
}
// try to push the colliding object
// if this is done before the alignment it can happen that the body walks into the object it is pushing
if (isPusher && Math.Abs(offset.X) > Math.Abs(offset.Y))
{
var pushRectangle = new Box(
body.Position.X + offset.X + body.OffsetX, body.Position.Y + body.OffsetY, body.Position.Z, body.Width, body.Height, body.Depth);
Game1.GameManager.MapManager.CurrentMap.Objects.PushObject(
pushRectangle, new Vector2(Math.Sign(offset.X), 0), PushableComponent.PushType.Continues);
}
}
}
if (offset.Y != 0)
{
var collidingBox = Box.Empty;
// move vertically
if (!Collision(body, body.Position.X, body.Position.Y + offset.Y, offset.Y < 0 ? 1 : 3, collisionTypes, ignoreField, ref collidingBox))
{
body.Position.Y += offset.Y;
}
else
{
var nullBox = Box.Empty;
collisionType |= offset.Y < 0 ? Values.BodyCollision.Top : Values.BodyCollision.Bottom;
collisionType |= Values.BodyCollision.Vertical;
// try to move around the object if there is space around it
if (slide)
{
var sliderOffset = Math.Abs(offset.Y * 0.5f);
if (offset.X >= 0 && !Collision(body, body.Position.X + body.MaxSlideDistance,
body.Position.Y + offset.Y, offset.Y < 0 ? 1 : 3, collisionTypes, ignoreField, ref nullBox))
{
body.SlideOffset.X += sliderOffset;
}
else if (offset.X <= 0 && !Collision(body, body.Position.X - body.MaxSlideDistance,
body.Position.Y + offset.Y, offset.Y < 0 ? 1 : 3, collisionTypes, ignoreField, ref nullBox))
{
body.SlideOffset.X -= sliderOffset;
}
}
// align with the floor
if (offset.Y < 0 &&
Math.Abs(body.Position.Y - (collidingBox.Front - body.OffsetY)) < Math.Abs(offset.Y) &&
!Collision(body, body.Position.X, collidingBox.Front - body.OffsetY, 1, collisionTypes, ignoreField, ref nullBox))
{
body.Position.Y = collidingBox.Front - body.OffsetY;
}
else if (offset.Y > 0 &&
Math.Abs(body.Position.Y - (collidingBox.Y - (body.Height + body.OffsetY))) < Math.Abs(offset.Y) &&
!Collision(body, body.Position.X, collidingBox.Y - (body.Height + body.OffsetY), 3, collisionTypes, ignoreField, ref nullBox))
{
body.Position.Y = collidingBox.Y - (body.Height + body.OffsetY);
}
// try to push the colliding object
if (isPusher && Math.Abs(offset.X) < Math.Abs(offset.Y))
{
var pushRectangle = new Box(
body.Position.X + body.OffsetX, body.Position.Y + offset.Y + body.OffsetY, body.Position.Z, body.Width, body.Height, body.Depth);
Game1.GameManager.MapManager.CurrentMap.Objects.PushObject(
pushRectangle, new Vector2(0, Math.Sign(offset.Y)), PushableComponent.PushType.Continues);
}
}
}
return collisionType;
}
private Values.BodyCollision UpdateVelocityZ(BodyComponent body)
{
var collision = Values.BodyCollision.None;
if (body.IgnoresZ)
{
body.IsGrounded = false;
return collision;
}
// let the body fall
var floorHeight = 0f;
if (!body.IgnoreHeight)
{
// get the position of the floor at the position of the body
var depthBox = new Box(
body.Position.X + body.OffsetX, body.Position.Y + body.OffsetY,
body.Position.Z - body.Depth + 1,
body.Width, body.Height, body.Depth);
floorHeight = Game1.GameManager.MapManager.CurrentMap.Objects.GetDepth(
depthBox, body.CollisionTypes, body.JumpStartHeight + body.MaxJumpHeight);
}
body.Velocity.Z += body.Gravity * Game1.TimeMultiplier;
body.Velocity.Z = Math.Clamp(body.Velocity.Z, -6, 6);
// move the body up or down as long as it is not hitting the floor
if (body.Position.Z + body.Velocity.Z * Game1.TimeMultiplier > floorHeight &&
(!body.IsGrounded || body.Velocity.Z >= 0 || Math.Abs(floorHeight - body.Position.Z) > 2))
{
// set jump height at beginning of the jump
if (body.IsGrounded)
body.JumpStartHeight = body.Position.Z;
body.Position.Z += body.Velocity.Z * Game1.TimeMultiplier;
body.IsGrounded = false;
}
else
{
// spawn splash animation
if (body.CurrentFieldState.HasFlag(MapStates.FieldStates.Water) && !body.IgnoreHeight && body.Velocity.Z < -0.5f)
{
var splashAnimator = new ObjAnimator(body.Owner.Map, 0, 0, 0, 3, Values.LayerPlayer, "Particles/splash", "idle", true);
splashAnimator.EntityPosition.Set(new Vector2(
body.Position.X + body.OffsetX + body.Width / 2f,
body.Position.Y + body.OffsetY + body.Height - body.Position.Z - 3));
Game1.GameManager.MapManager.CurrentMap.Objects.SpawnObject(splashAnimator);
}
// bounce from the ground but not on the water
if (body.Velocity.Z * body.Bounciness < -0.4f &&
!body.CurrentFieldState.HasFlag(MapStates.FieldStates.DeepWater))
body.Velocity.Z *= -body.Bounciness;
else
body.Velocity.Z = 0;
if (!body.IsGrounded)
collision |= Values.BodyCollision.Floor;
// don't move the body on top of the object it is colliding
if (body.Position.Z > floorHeight ||
Math.Abs(body.Position.Z - floorHeight) <= 3)
body.Position.Z = floorHeight;
body.JumpStartHeight = body.Position.Z;
body.IsGrounded = true;
}
return collision;
}
private void UpdateHole(BodyComponent body)
{
// check for collisions with holes
if (body.Position.Z > 0)
{
body.WasHolePulled = false;
return;
}
var bodyBox = body.BodyBox.Box;
var bodyArea = bodyBox.Width * bodyBox.Height;
var bodyBoxCenter = body.BodyBox.Box.Center;
var holeCollisionCoM = Vector2.Zero;
var holeCollisionArea = 0.0f;
var noneCollisionCoM = bodyBoxCenter;
var noneCollisionArea = bodyBox.Width * bodyBox.Height;
_holeList.Clear();
Game1.GameManager.MapManager.CurrentMap.Objects.GetComponentList(
_holeList, (int)bodyBox.X, (int)bodyBox.Y, (int)bodyBox.Width, (int)bodyBox.Height, CollisionComponent.Mask);
foreach (var hole in _holeList)
{
if (!hole.IsActive)
continue;
var collisionObject = hole.Components[CollisionComponent.Index] as CollisionComponent;
var collidingBox = Box.Empty;
if ((collisionObject.CollisionType & Values.CollisionTypes.Hole) == 0 ||
!collisionObject.Collision(bodyBox, 0, 0, ref collidingBox))
continue;
var collidingRec = bodyBox.Rectangle().GetIntersection(collidingBox.Rectangle());
var collidingArea = collidingRec.Width * collidingRec.Height;
// center of mass for the holes
holeCollisionCoM =
holeCollisionCoM * (holeCollisionArea / (holeCollisionArea + collidingArea)) +
collidingRec.Center * (collidingArea / (holeCollisionArea + collidingArea));
// this makes sure to not cancle out two holes pulling the body into different directions; otherwise the body would be able to walk between them if he is aligned with the
if (collidingArea == holeCollisionArea && holeCollisionCoM.X == bodyBoxCenter.X && collidingRec.Width * 2 != bodyBox.Width)
holeCollisionCoM.X -= 4;
if (collidingArea == holeCollisionArea && holeCollisionCoM.Y == bodyBoxCenter.Y && collidingRec.Height * 2 != bodyBox.Height)
holeCollisionCoM.Y += 4;
holeCollisionArea += collidingArea;
}
// calculate the new centers of mass and collision/none collision areas
noneCollisionCoM += (noneCollisionCoM - holeCollisionCoM) * (holeCollisionArea / noneCollisionArea);
noneCollisionArea -= holeCollisionArea;
body.SpeedMultiply = 1 - holeCollisionArea / bodyArea;
// the direction of the force applied to the body goes from the CoM of the body rectangle that is not colliding
// to the CoM of the body rectangle that is colliding
var holeDirection = holeCollisionCoM - noneCollisionCoM;
if (holeDirection != Vector2.Zero)
holeDirection.Normalize();
body.IsAbsorbed = false;
var collisionAreaPercentage = holeCollisionArea / bodyArea;
// the body is getting absorbed
if (holeCollisionArea >= bodyArea * body.AbsorbPercentage)
{
// absorption gets set to zero if the body jumped into the hole
// fixes a bug where the player can push an object on the other side of a hole while falling into it
if (!body.WasHolePulled)
body.HoleAbsorption = Vector2.Zero;
body.Velocity = Vector3.Zero;// *= (float)Math.Pow(0.85f, Game1.TimeMultiplier);
body.HoleAbsorption *= (float)Math.Pow(0.85f, Game1.TimeMultiplier);
body.HoleAbsorb?.Invoke();
body.IsAbsorbed = true;
}
// body is getting pulled towards the hole
else if (collisionAreaPercentage > body.AbsorbStop)
{
var holePull = new Vector2(holeDirection.X, holeDirection.Y) * collisionAreaPercentage * 0.5f;
// calculate the new direction of the hole pull
var oldPercentage = (float)Math.Pow(0.8f, Game1.TimeMultiplier);
body.HoleAbsorption = body.HoleAbsorption * oldPercentage +
holePull * (1 - oldPercentage);
body.HoleOnPull?.Invoke(holePull, collisionAreaPercentage);
body.WasHolePulled = true;
}
// stop the absorption
else if (body.HoleAbsorption != Vector2.Zero)
{
body.HoleAbsorption = Vector2.Zero;
body.HoleOnPull?.Invoke(Vector2.Zero, collisionAreaPercentage);
body.WasHolePulled = false;
}
}
public static bool Collision(BodyComponent body, float posX, float posY, int direction,
Values.CollisionTypes collisionTypes, bool ignoreField, ref Box collidingBox)
{
// the +2 is to allow the body to move onto objects that are up to 2 higher
var box = new Box(posX + body.OffsetX, posY + body.OffsetY,
Math.Min(body.JumpStartHeight + body.MaxJumpHeight, body.Position.Z + 2), body.Width, body.Height, body.Depth);
var oldBox = new Box(body.Position.X + body.OffsetX, body.Position.Y + body.OffsetY,
Math.Min(body.JumpStartHeight + body.MaxJumpHeight, body.Position.Z), body.Width, body.Height, body.Depth);
// check if the body is inside his allowed field or if he already left it
if (!ignoreField && body.FieldRectangle.Width > 0 &&
!body.FieldRectangle.Contains(box.Rectangle()) && body.FieldRectangle.Contains(oldBox.Rectangle()))
return true;
var cBox = Box.Empty;
if (Game1.GameManager.MapManager.CurrentMap.Objects.Collision(
box, body.IgnoreInsideCollision ? oldBox : Box.Empty, collisionTypes, body.CollisionTypesIgnore, direction, body.Level, ref cBox))
{
collidingBox = cBox;
return true;
}
return false;
}
}
}