/// <summary>Check if a mover is above a static object by physics heightmap</summary> private static bool DoAboveCheck(AnimationSet staticAnimationSet, AnimationSet moverAnimationSet, Position staticPosition, Position moverPosition, bool staticFlipX, bool moverFlipX) { HeightmapView heightmapView = new HeightmapView(staticAnimationSet.Heightmap, staticPosition, staticFlipX); if (!heightmapView.BoundsZ.Contains(moverPosition.Z)) { return(false); // out of range } var staticRangeX = heightmapView.BoundsX; var moverRangeX = new Range(moverAnimationSet.physicsStartX, moverAnimationSet.physicsEndX).MaybeFlip(moverFlipX) + moverPosition.X; var rangeX = moverRangeX.Clip(staticRangeX.start, staticRangeX.end); for (int x = rangeX.start; x < rangeX.end; x++) { int height = heightmapView.GetUnelevatedHeightAt(x, moverPosition.Z); if (height == 0 || height == Heightmap.Infinity) { continue; } height += heightmapView.position.Y; if (moverPosition.Y >= height) { return(true); // <- above the object at at least one position } } return(false); }
public void DrawHeightmapFlat(HeightmapView heightmapView, SortedList <int, Color> heightColorGradient) { int startX, endX; HeightmapDrawRangeHelper(heightmapView, out startX, out endX); if (endX <= startX) { return; // Off screen } int xFlipFactor = heightmapView.flipX ? -1 : 1; for (int z = heightmapView.heightmap.EndZ - 1; z >= heightmapView.heightmap.StartZ; z--) // From back to front of heightmap { for (int x = startX; x < endX; x++) { byte height = heightmapView.heightmap[x * xFlipFactor, z]; if (height == heightmapView.heightmap.DefaultHeight) { continue; } // Draw top surface DrawPixel(new Position(heightmapView.position.X + x, 0, heightmapView.position.Z + z), heightColorGradient.GetColorFromGradient(height + heightmapView.position.Y), 0); } } }
private bool IsRestingOn(int top, int bottom) { var bottomHeightmapView = new HeightmapView(moverHeightmaps[bottom], moverInitialPositions[bottom], moverInitialFacingLeft[bottom]); var targetY = moverInitialPositions[top].Y - bottomHeightmapView.position.Y; if (targetY <= 0 ) // <- This is just safety if external range checks didn't exclude something correctly (this was a bug with infinite-height objects, before I made them unstackable -AR) { return(false); } if (moverHeightmaps[top] != null) { // // Heightmap vs Heightmap var topHeightmapView = new HeightmapView(moverHeightmaps[top], new Position(moverInitialPositions[top].X, 0, moverInitialPositions[top].Z), moverInitialFacingLeft[top]); var xzIntersection = Rectangle.Intersect(bottomHeightmapView.Bounds, topHeightmapView.Bounds); for (var z = 0; z < xzIntersection.Height; z++) { for (var x = 0; x < xzIntersection.Width; x++) { var xx = x + xzIntersection.X; var zz = z + xzIntersection.Y; if (topHeightmapView.GetUnelevatedHeightAt(xx, zz) != 0) { if (bottomHeightmapView.GetUnelevatedHeightAt(xx, zz) == targetY) { return(true); } } } } } else { // // Heightmap vs Character for (var x = moverBoundsX[top].start; x < moverBoundsX[top].end; x++) { if (bottomHeightmapView.GetUnelevatedHeightAt(x, moverInitialPositions[top].Z) == targetY) { return(true); } } } return(false); }
public static int GetDistanceSquaredToLocalPlayer(AnimationSet animationSet, Position position, bool facingLeft, IGameState gameState, int localPlayerBits) { int bestDistanceSquared = Int32.MaxValue; if (animationSet != null && animationSet.Heightmap != null) // Sound source has some (spatial) volume { var heightmapView = new HeightmapView(animationSet.Heightmap, position, facingLeft); Rectangle heightmapXZ = heightmapView.Bounds; // TODO: Stop assuming a height, and get a real AABB from the heightmap (requires Heightmap cache its own AABB) const int guessHeight = 50; AABB heightmapAABB = new AABB(heightmapXZ.Left, heightmapXZ.Right - 1, position.Y, position.Y + guessHeight, heightmapXZ.Y, heightmapXZ.Y + heightmapXZ.Height - 1); for (int i = 0; i < gameState.MaxPlayers; i++) // For each possible player { if (((1 << i) & localPlayerBits) != 0) // This player is interactive locally { var playerPosition = gameState.GetPlayerPosition(i); if (playerPosition != null) { int distanceSquared = heightmapAABB.DistanceSquaredTo(playerPosition.Value); if (distanceSquared < bestDistanceSquared) { bestDistanceSquared = distanceSquared; } } } } } else // Sound source is a point source { for (int i = 0; i < gameState.MaxPlayers; i++) // For each possible player { if (((1 << i) & localPlayerBits) != 0) // This player is interactive locally { var playerPosition = gameState.GetPlayerPosition(i); if (playerPosition != null) { int distanceSquared = Position.DistanceSquared(position, playerPosition.Value); if (distanceSquared < bestDistanceSquared) { bestDistanceSquared = distanceSquared; } } } } } return(bestDistanceSquared); }
/// <summary>Constrain rendering to the display bounds</summary> void HeightmapDrawRangeHelper(HeightmapView heightmapView, out int startX, out int endX) { if (heightmapView.flipX) { startX = Math.Max(1 - heightmapView.heightmap.EndX, displayBounds.Left - heightmapView.position.X); endX = Math.Min(1 - heightmapView.heightmap.StartX, displayBounds.Right - heightmapView.position.X); } else { startX = Math.Max(heightmapView.heightmap.StartX, displayBounds.Left - heightmapView.position.X); endX = Math.Min(heightmapView.heightmap.EndX, displayBounds.Right - heightmapView.position.X); } }
private bool SolidVsSolidIntersection(int first, int second) { // NOTE: Calling code has done a bounds check on all three axes var firstHeightmapView = new HeightmapView(moverHeightmaps[first], moverInitialPositions[first], moverInitialFacingLeft[first]); var secondHeightmapView = new HeightmapView(moverHeightmaps[second], moverInitialPositions[second], moverInitialFacingLeft[second]); var xzIntersection = Rectangle.Intersect(firstHeightmapView.Bounds, secondHeightmapView.Bounds); for (var z = 0; z < xzIntersection.Height; z++) { for (var x = 0; x < xzIntersection.Width; x++) { var xx = x + xzIntersection.X; var zz = z + xzIntersection.Y; var firstHeight = firstHeightmapView.GetUnelevatedHeightAt(xx, zz); var secondHeight = secondHeightmapView.GetUnelevatedHeightAt(xx, zz); if (firstHeight == 0 || secondHeight == 0) { continue; } if (firstHeight == Heightmap.Infinity) { firstHeight = MaximumHeight; } if (secondHeight == Heightmap.Infinity) { secondHeight = MaximumHeight; } var firstRangeY = new Range(firstHeightmapView.position.Y, firstHeightmapView.position.Y + firstHeight); var secondRangeY = new Range(secondHeightmapView.position.Y, secondHeightmapView.position.Y + secondHeight); if (Range.Overlaps(firstRangeY, secondRangeY)) { return(true); } } } return(false); }
public void DrawHeightmapSolid(HeightmapView heightmapView, SortedList <int, Color> heightColorGradient) { int startX, endX; HeightmapDrawRangeHelper(heightmapView, out startX, out endX); if (endX <= startX) { return; // Off screen } int xFlipFactor = heightmapView.flipX ? -1 : 1; for (int z = heightmapView.heightmap.EndZ - 1; z >= heightmapView.heightmap.StartZ; z--) // From back to front of heightmap { for (int x = startX; x < endX; x++) { byte height = heightmapView.heightmap[x * xFlipFactor, z]; if (height == heightmapView.heightmap.DefaultHeight) { continue; } byte nextHeight = heightmapView.heightmap[x * xFlipFactor, z - 1]; if (nextHeight != heightmapView.heightmap.DefaultHeight && nextHeight > height) { continue; // Next row will cover this one entirely } // Draw top surface const int zTestOffset = 1; // <- The top surface should be "under" any other pixels DrawPixel(heightmapView.position + new Position(x, height, z), heightColorGradient.GetColorFromGradient(height + heightmapView.position.Y), zTestOffset); if (nextHeight != heightmapView.heightmap.DefaultHeight && nextHeight == height) { continue; // Next row covers this one's "solid" section } // Draw solidness for (int h = height + heightmapView.position.Y - 1; h >= heightmapView.position.Y; h--) { Color c = Color.Lerp(heightColorGradient.GetColorFromGradient(h), Color.Black, 0.6f); DrawPixel(new Position(heightmapView.position.X + x, h, heightmapView.position.Z + z), c, 0); } } } }
private bool SolidVsCharacterIntersection(int solid, int character) { // NOTE: Calling code has done a bounds check on all three axes var heightmapView = new HeightmapView(moverHeightmaps[solid], moverInitialPositions[solid], moverInitialFacingLeft[solid]); var heightmapRangeX = heightmapView.BoundsX; var characterX = moverInitialPositions[character].X; var characterY = moverInitialPositions[character].Y; var characterZ = moverInitialPositions[character].Z; var characterRangeX = new Range(characterX + moverCharacterPhysics[character].startX, characterX + moverCharacterPhysics[character].endX); var characterRangeY = new Range(characterY, characterY + moverCharacterPhysics[character].height); Debug.Assert(heightmapView.BoundsZ.Contains(characterZ)); var xIntersection = heightmapRangeX.Clip(characterRangeX.start, characterRangeX.end); for (var x = xIntersection.start; x < xIntersection.end; x++) { var solidHeight = heightmapView.GetUnelevatedHeightAt(x, characterZ); if (solidHeight == 0) { continue; } if (solidHeight == Heightmap.Infinity) { solidHeight = MaximumHeight; } var solidRangeY = new Range(heightmapView.position.Y, heightmapView.position.Y + solidHeight); if (Range.Overlaps(solidRangeY, characterRangeY)) { return(true); } } return(false); }
public void DrawShadowReceiver(DrawContext context, Sprite whitemask, ShadowReceiver shadowReceiver, Position shadowReceiverPosition, bool shadowReceiverFlipX) { Bounds receiverBounds = new Bounds(whitemask.WorldSpaceBounds).MaybeFlipX(shadowReceiverFlipX) + shadowReceiverPosition.ToWorldZero; bool startedReceivingShadows = false; // For each shadow caster: Debug.Assert(shadowCasterXBounds.Count == shadowCasters.Count); for (int i = 0; i < shadowCasterXBounds.Count; i++) { StartEnd casterWorldXBounds = shadowCasterXBounds[i]; if (casterWorldXBounds.start >= receiverBounds.endX || receiverBounds.startX >= casterWorldXBounds.end) { continue; // Horizontally out of range } var shadowCaster = shadowCasters[i]; // Determine the ground height of the shadow: int groundHeight; if (shadowReceiver.heightmap.HasData) { var heightmapView = new HeightmapView(shadowReceiver.heightmap, shadowReceiverPosition, shadowReceiverFlipX); int startX = shadowCaster.position.X + shadowCaster.physicsStartX; int endX = shadowCaster.position.X + shadowCaster.physicsEndX; if (endX <= startX) { endX = startX + 1; // <- whoops our shadow caster has no width... quick fix-up for now... } groundHeight = heightmapView.GetHeightForShadow(startX, endX, shadowCaster.position.Z, shadowReceiver.heightmapExtendDirection); } else { groundHeight = shadowReceiver.heightmap.DefaultHeight + shadowReceiverPosition.Y; } int shadowDifference = shadowCaster.position.Y - groundHeight; // NOTE: shadowOffset is pre-flipped Position shadowPosition = new Position(shadowCaster.position.X, groundHeight, shadowCaster.position.Z) + shadowCaster.shadowOffset; if (shadowCaster.shadows[0].startHeight > shadowDifference) { continue; // Out of range } int shadowIndex = 0; while (shadowIndex + 1 < shadowCaster.shadows.Count) { if (shadowCaster.shadows[shadowIndex + 1].startHeight > shadowDifference) { break; } shadowIndex++; } Sprite shadowSprite; if (!shadowCaster.shadows[shadowIndex].shadowSpriteRef.ResolveBestEffort(out shadowSprite)) { continue; } // Determine whether the shadow overlaps the whitemask Bounds shadowBounds = new Bounds(shadowSprite.WorldSpaceBounds).MaybeFlipX(shadowCaster.flipX) + shadowPosition.ToWorldZero; Bounds worldZeroDrawBounds = Bounds.Intersection(receiverBounds, shadowBounds); if (!worldZeroDrawBounds.HasPositiveArea) { continue; } if (!startedReceivingShadows) { startedReceivingShadows = true; context.SetupShadowReceiver(whitemask, shadowReceiver, shadowReceiverPosition, shadowReceiverFlipX); } // Drawing with clipping: { Rectangle clipRect = new Rectangle // <- Region within the source rectangle (NOTE: initially with Y+ up, we fix later) { X = worldZeroDrawBounds.startX - shadowBounds.startX, Y = worldZeroDrawBounds.startY - shadowBounds.startY, Width = worldZeroDrawBounds.endX - worldZeroDrawBounds.startX, Height = worldZeroDrawBounds.endY - worldZeroDrawBounds.startY, }; // Flipping: if (shadowCaster.flipX) { clipRect.X = shadowSprite.sourceRectangle.Width - (clipRect.X + clipRect.Width); // RegionFlipX } // IMPORTANT: Convert clipRect from Y+ Up coordinates to correct Texel coordinates clipRect.Y = shadowSprite.sourceRectangle.Height - (clipRect.Y + clipRect.Height); // RegionFlipY // Turn it into a source rectangle within the texture: clipRect.X += shadowSprite.sourceRectangle.X; clipRect.Y += shadowSprite.sourceRectangle.Y; Vector2 displayPosition = new Vector2(worldZeroDrawBounds.startX, -(worldZeroDrawBounds.startY + clipRect.Height)); // NOTE: don't need to consider origin, because it's built into the draw bounds context.SpriteBatch.Draw(shadowSprite.texture, displayPosition, clipRect, shadowCaster.color, 0f, Vector2.Zero, 1, shadowCaster.flipX ? SpriteEffects.FlipHorizontally : SpriteEffects.None, 0); } } if (startedReceivingShadows) { context.TeardownShadowReceiver(); } }
public void DoSeparation(UpdateContext updateContext) { // // Process anyone requesting to be updated before separation // foreach (var i in collidersToUpdateBeforeSeparation) { Actor actor = (Actor)colliderEntries[i].owner; // <- will always succeed, because we registered it that way HeightmapView heightmapView = new HeightmapView(actor.animationSet.Heightmap, actor.position, actor.facingLeft); ChangeCollider(i, heightmapView); } // // Remove everyone from static geometry // for (int i = 0; i < moverCount; i++) { if (moverWeDoNotMove[i]) { continue; } if (moverActors[i].IncomingConnection != null) { continue; // Don't separate if we're attached to someone } var cpi = moverCharacterPhysics[i]; int simpleGroundHeight = CharacterPhysics.GroundHeight(ref cpi, this, moverActors[i].position, true); if (simpleGroundHeight <= moverActors[i].position.Y) { continue; // We are in open space } // // Oh no! We are stuck in the level! // // Attempt 1: Move back to start point, and then try moving to new position: // (Doing this first, to try to prevent tunneling behaviour - also probably faster than TryToFitNearestInZSlice) Position moveDelta = moverActors[i].position - moverInitialPositions[i]; if (moveDelta != Position.Zero && // <- Can't undo non-moves. moveDelta.ManhattenLength <= 8) // <- Don't try to undo huge moves, as they may be teleports/animations, and we could get snagged on the way there. { var groundHeightAtInitial = CharacterPhysics.GroundHeight(ref cpi, this, moverInitialPositions[i], true); if (groundHeightAtInitial <= moverInitialPositions[i].Y) // <- OK at initial position { // Attempt to move towards target: Position target = moverActors[i].position; moverActors[i].position = moverInitialPositions[i]; CharacterPhysics.TryMove(ref moverCharacterPhysics[i], this, moveDelta, false, ref moverActors[i].position, true); continue; // Success! } } // Attempt 2: Attempt to extricate ourselves from the situation directly #region // a whole bunch of code { int statsZSliceCount = 0; long statsVoxelCount = 0; Position desiredPosition = moverActors[i].position; // Ensure we didn't come out of the level entirely if (desiredPosition.X < StartX) { desiredPosition.X = StartX; } if (desiredPosition.X >= EndX) { desiredPosition.X = EndX - 1; } if (desiredPosition.Y < 0) { desiredPosition.Y = 0; } if (desiredPosition.Z < StartZ) { desiredPosition.Z = StartZ; } if (desiredPosition.Z >= EndZ) { desiredPosition.Z = EndZ - 1; } // Grab the nearest point on our current plane: const int maxRadiusXY = 128; Point nearest = TryToFitNearestInZSlice(moverCharacterPhysics[i].startX, moverCharacterPhysics[i].endX, moverCharacterPhysics[i].height, desiredPosition.X, desiredPosition.Y, moverActors[i].position.Y, desiredPosition.X - maxRadiusXY, desiredPosition.X + maxRadiusXY, desiredPosition.Z, desiredPosition.Y - maxRadiusXY, desiredPosition.Y + maxRadiusXY, moverActors[i], true); statsZSliceCount++; statsVoxelCount += (maxRadiusXY * maxRadiusXY * 4); Position bestPosition; int bestDistance; if (nearest != NoFitFound) { bestDistance = Math.Abs(desiredPosition.X - nearest.X) + Math.Abs(desiredPosition.Y - nearest.Y); bestPosition = new Position(nearest.X, nearest.Y, desiredPosition.Z); // Debugging: If check that we're not ending up back in the same place (why did we try to separate, if we fit?) // If this assert fires, there is a bug in the separation algorithm. // NOTE: checks vs non-clipped position! Debug.Assert(Position.ManhattenDistance(moverActors[i].position, bestPosition) > 0, "SEPARATION DID NOTHING. Actor is: " + moverActors[i].ToString() + "\n\nIf you can reproduce this, please report to Andrew. Otherwise hit \"Ignore\" and carry on."); } else { bestPosition = default(Position); bestDistance = int.MaxValue; } // Search forwards and backwards for better positions on nearby planes: const int maxRadiusZ = 10; int searchBoundStartZ = Math.Max(StartZ, desiredPosition.Z - maxRadiusZ); int searchBoundEndZ = Math.Min(EndZ, desiredPosition.Z + maxRadiusZ); // Searching forwards: (NOTE: Copy-paste with searching backwards) int searchZ = desiredPosition.Z - 1; // NOTE: Checking vs bestDistance each time, so we might be able to early-out while (searchZ >= searchBoundStartZ && (desiredPosition.Z - searchZ) < bestDistance) // <- IMPORTANT: not doing maths on bestDistance, as it can be int.MaxValue! { Debug.Assert(bestDistance > 0); int radiusXY = Math.Min(bestDistance, maxRadiusXY); // <- hopefully reduces search range nearest = TryToFitNearestInZSlice(moverCharacterPhysics[i].startX, moverCharacterPhysics[i].endX, moverCharacterPhysics[i].height, desiredPosition.X, desiredPosition.Y, moverActors[i].position.Y, desiredPosition.X - radiusXY, desiredPosition.X + radiusXY, searchZ, desiredPosition.Y - radiusXY, desiredPosition.Y + radiusXY, moverActors[i], true); statsZSliceCount++; statsVoxelCount += (radiusXY * radiusXY * 4); if (nearest != NoFitFound) { int distance = Math.Abs(desiredPosition.X - nearest.X) + Math.Abs(desiredPosition.Y - nearest.Y) + Math.Abs(desiredPosition.Z - searchZ); if (distance < bestDistance) { bestDistance = distance; bestPosition = new Position(nearest.X, nearest.Y, searchZ); } } searchZ--; } // Searching backwards: (NOTE: Copy-paste with searching forwards) searchZ = desiredPosition.Z + 1; // NOTE: Checking vs bestDistance each time, so we might be able to early-out while (searchZ < searchBoundEndZ && (searchZ - desiredPosition.Z) < bestDistance) // <- IMPORTANT: not doing maths on bestDistance, as it can be int.MaxValue! { Debug.Assert(bestDistance > 0); int radiusXY = Math.Min(bestDistance, maxRadiusXY); // <- hopefully reduces search range nearest = TryToFitNearestInZSlice(moverCharacterPhysics[i].startX, moverCharacterPhysics[i].endX, moverCharacterPhysics[i].height, desiredPosition.X, desiredPosition.Y, moverActors[i].position.Y, desiredPosition.X - radiusXY, desiredPosition.X + radiusXY, searchZ, desiredPosition.Y - radiusXY, desiredPosition.Y + radiusXY, moverActors[i], true); statsZSliceCount++; statsVoxelCount += (radiusXY * radiusXY * 4); if (nearest != NoFitFound) { int distance = Math.Abs(desiredPosition.X - nearest.X) + Math.Abs(desiredPosition.Y - nearest.Y) + Math.Abs(desiredPosition.Z - searchZ); if (distance < bestDistance) { bestDistance = distance; bestPosition = new Position(nearest.X, nearest.Y, searchZ); } } searchZ++; } #if DEBUG // Separation stats { bool isActuallyTheMainSimulation = (updateContext.DebugNetworkUnsafeHasLocalSettings); // <- Somewhat hacky mechanisim so we don't get debug writes for "alternate universes" when debugging if (isActuallyTheMainSimulation) { Debug.WriteLine("Separation of \"" + moverActors[i] + "\" searched " + statsZSliceCount + " Z slices, checking " + statsVoxelCount + " voxels; moved from " + moverActors[i].position + " to " + bestPosition + "."); } } #endif if (bestDistance != int.MaxValue) // <- Did we find a suitable position? { moverActors[i].position = bestPosition; continue; } } #endregion // // At this point... holy crap... how did you manage to get an object here?? // stuckObjects.Add(moverActors[i]); // <- game state can deal with it. } }