void DrawBorder(Vector2 position, int width, int height) { TileIndex borderTiles = new TileIndex(0, 1); for (int j = 0; j < height; j++) { int row; if (j == 0) row = 0; else if (j == height - 1) row = 2; else row = 1; for (int i = 0; i < width; i++) { int column; if (i == 0) column = 0; else if (i == width - 1) column = 2; else column = 1; TileIndex tile = borderTiles + new TileIndex(column, row); TileEngine.DrawTile(Assets.UITiles, tile, position + new Vector2(i, j) * Assets.CellSize); } } }
public void Update() { List<Action> diagnostics = new List<Action>(); AnimationTimer += 1; bool advanceFrame = false; if (AnimationTimer >= Assets.AnimationPeriod) { AnimationTimer -= Assets.AnimationPeriod; advanceFrame = true; } //========================================================================================== // Conversations: //========================================================================================== List<Creature> availableConversers = new List<Creature>(); foreach (Creature creature in Creatures) { if (creature != Player) { float distance = (creature.Position - Player.Position).Length(); if (creature.Conversation.Length > 0 && distance < 96) { availableConversers.Add(creature); } if (creature.CanKill && State == GameState.Playing && distance < 48) { State = GameState.Losing; Engine.StopMusic(2.0f); } if (creature.CanWin && State == GameState.Playing && distance < 48) { State = GameState.Winning; Engine.StopMusic(2.0f); } } } Conversers.RemoveAll(x => !availableConversers.Contains(x)); if (!InConversation && availableConversers.Count > 0 && Engine.GetKeyDown(Key.J)) { // Begin a conversation: Creature other = availableConversers[0]; Conversers.Add(other); ConversationPage = 0; if (other.Thought == Thought.WaitToJoin) other.Thought = Thought.Follow; } else if (InConversation && Engine.GetKeyDown(Key.J)) { // Continue a conversation: ConversationPage += 1; } //========================================================================================== // Player input: //========================================================================================== Vector2 input = Vector2.Zero; if (Engine.GetKeyHeld(Key.A)) input.X -= 1; if (Engine.GetKeyHeld(Key.D)) input.X += 1; if (Engine.GetKeyHeld(Key.W)) input.Y -= 1; if (Engine.GetKeyHeld(Key.S)) input.Y += 1; Player.Movement = input; if (input.Length() != 0) { ShowMoveTip = false; } //========================================================================================== // AI: //========================================================================================== foreach (Creature creature in Creatures) { if (creature.Thought == Thought.Follow) { Vector2 vectorToPlayer = Player.Position - creature.Position; float distanceToPlayer = vectorToPlayer.Length(); if (distanceToPlayer > creature.FollowDistanceMin && distanceToPlayer < creature.FollowDistanceMax) { creature.Movement = vectorToPlayer.Normalized(); } else { creature.Movement = Vector2.Zero; } } } //========================================================================================== // Physics: //========================================================================================== foreach (Creature creature in Creatures) { if (State != GameState.Playing) { creature.Movement = Vector2.Zero; creature.Velocity = Vector2.Zero; } creature.Velocity += creature.Movement * creature.MaxAcceleration * Engine.TimeDelta; creature.Velocity.X = Clamp(creature.Velocity.X, -creature.Speed, creature.Speed); creature.Velocity.Y = Clamp(creature.Velocity.Y, -creature.Speed, creature.Speed); // Update sprite facing: if (creature.Movement.X < 0) creature.Facing = TextureMirror.Horizontal; if (creature.Movement.X > 0) creature.Facing = TextureMirror.None; // Update position and collide: { // These bounds describe the "solid" part of the entity; it is independent of position. Bounds2 creatureShape = new Bounds2( new Vector2(1 / 8f, 6 / 8f) * Assets.CellSize, new Vector2(6 / 8f, 2 / 8f) * Assets.CellSize); Vector2 motion = creature.Velocity * Engine.TimeDelta; TileIndex nearest = GetCellAt(creature.Position, Assets.WallTiles); // Find everything that could be collided with: List<Bounds2> obstacles = new List<Bounds2>(); for (int row = nearest.Row - 2; row <= nearest.Row + 2; row++) { for (int column = nearest.Column - 2; column <= nearest.Column + 2; column++) { if (Obstacles[column, row]) { Bounds2 obstacleBounds = new Bounds2( new Vector2(column, row) * Assets.CellSize, Assets.CellSize); // The effective bounds are the sum of the obstacle's and the mover's bounds: Vector2 min = obstacleBounds.Min - creatureShape.Max; Vector2 max = obstacleBounds.Max - creatureShape.Min; Bounds2 totalBounds = new Bounds2(min, max - min); obstacles.Add(totalBounds); if (Game.DebugCollision && creature == Player) { diagnostics.Add(() => Engine.DrawRectEmpty(obstacleBounds.Translated(Origin), Color.Green)); } } } } // Calculate collision along the X and Y axes independently. // Each calculation considers only motion along that axis. // Performing these steps sequentially ensures that objects can't move diagonally through other objects. // X step: Vector2 position = creature.Position; Vector2 newPosition = position + new Vector2(motion.X, 0); foreach (Bounds2 bounds in obstacles) { // Leftward: if (motion.X < 0 && position.Y > bounds.Position.Y && position.Y < bounds.Position.Y + bounds.Size.Y) { float limit = bounds.Position.X + bounds.Size.X; if (position.X >= limit && newPosition.X < limit) newPosition.X = limit; } // Rightward: if (motion.X > 0 && position.Y > bounds.Position.Y && position.Y < bounds.Position.Y + bounds.Size.Y) { float limit = bounds.Position.X; if (position.X <= limit && newPosition.X > limit) newPosition.X = limit; } } creature.Position.X = newPosition.X; // Y step: position = creature.Position; newPosition = position + new Vector2(0, motion.Y); foreach (Bounds2 bounds in obstacles) { // Upward: if (motion.Y < 0 && position.X > bounds.Position.X && position.X < bounds.Position.X + bounds.Size.X) { float limit = bounds.Position.Y + bounds.Size.Y; if (position.Y >= limit && newPosition.Y < limit) newPosition.Y = limit; } // Downward: if (motion.Y > 0 && position.X > bounds.Position.X && position.X < bounds.Position.X + bounds.Size.X) { float limit = bounds.Position.Y; if (position.Y <= limit && newPosition.Y > limit) newPosition.Y = limit; } } creature.Position.Y = newPosition.Y; if (Game.DebugCollision && creature == Player) { diagnostics.Add(() => { Engine.DrawRectEmpty(creatureShape.Translated(Origin + creature.Position), Color.Green); }); } } // Slow to a stop when there is no input -- separately on each axis: if (creature.Movement.X == 0) { float speed = Math.Abs(creature.Velocity.X); speed = Math.Max(0, speed - creature.Deceleration); creature.Velocity.X = Math.Sign(creature.Velocity.X) * speed; } if (creature.Movement.Y == 0) { float speed = Math.Abs(creature.Velocity.Y); speed = Math.Max(0, speed - creature.Deceleration); creature.Velocity.Y = Math.Sign(creature.Velocity.Y) * speed; } } //========================================================================================== // Scroll to keep the player onscreen: //========================================================================================== { Vector2 margin = new Vector2(300, 250); Origin.X = Clamp(Origin.X, -Player.Position.X + margin.X, -(Player.Position.X + Assets.PropTiles.DestinationSize.X) + Game.Resolution.X - margin.X); Origin.Y = Clamp(Origin.Y, -Player.Position.Y + margin.Y, -(Player.Position.Y + Assets.PropTiles.DestinationSize.Y) + Game.Resolution.Y - margin.Y); } //========================================================================================== // Draw the static part of the map: //========================================================================================== for (int row = 0; row < MapHeight; row++) { for (int column = 0; column < MapWidth; column++) { TileEngine.DrawTile(Assets.WallTiles, Walls[column, row], Origin + new Vector2(column, row) * Assets.CellSize); } } //========================================================================================== // Draw creatures back-to-front: //========================================================================================== foreach (Creature creature in Creatures.OrderBy(x => x.IsFlat ? 0 : 1).ThenBy(x => x.Position.Y)) { TileEngine.DrawTile(Assets.PropTiles, creature.Appearance[creature.Frame], Origin + creature.Position, mirror: creature.Facing); if (advanceFrame) { creature.Frame = (creature.Frame + 1) % creature.Appearance.Length; } } //========================================================================================== // Draw UI: //========================================================================================== if (InConversation) { string speech = Conversers[0].Conversation[ConversationPage]; int width = 16; int height = 3; Vector2 pos = new TileIndex((20 - width) / 2, 12 - height) * Assets.CellSize; DrawBorder(pos, width, height); pos += 0.5f * Assets.CellSize; TileEngine.DrawTileString(Assets.FontTiles, speech, pos); } else if (availableConversers.Count > 0) { string text = "\x18 Converse"; Vector2 pos = new Vector2((20 - (text.Length + 1) / 2) / 2, 11) * Assets.CellSize; TileEngine.DrawTileString(Assets.FontTiles, text, pos); } else if (ShowMoveTip) { string text = "\x1C \x1D \x1E \x1F Move"; Vector2 pos = new Vector2((20 - (text.Length + 1) / 2) / 2, 11) * Assets.CellSize; TileEngine.DrawTileString(Assets.FontTiles, text, pos); } //========================================================================================== // End of game cinematics: //========================================================================================== if (State == GameState.Losing) { if (advanceFrame) { EndGameFrame += 1; if (EndGameFrame == Assets.EndMusicFrame) Engine.PlayMusic(Assets.LosingMusic, fadeTime: 6.0f); } Color background = Color.Black.WithAlpha(EndGameFrame / 5f); Engine.DrawRectSolid(new Bounds2(Vector2.Zero, Game.Resolution), background); string text = "You died"; float textAlpha = Clamp((EndGameFrame - 8) / 5f, 0, 1); Color textColor = Assets.LosingTextColor.WithAlpha(textAlpha); Vector2 pos = new Vector2((20 - (text.Length + 1) / 2) / 2, 5) * Assets.CellSize; TileEngine.DrawTileString(Assets.FontTiles, text, pos, color: textColor); } else if (State == GameState.Winning) { if (advanceFrame) { EndGameFrame += 1; if (EndGameFrame == Assets.EndMusicFrame) Engine.PlayMusic(Assets.WinningMusic, fadeTime: 6.0f); } Color background = Color.White.WithAlpha(EndGameFrame / 5f); Engine.DrawRectSolid(new Bounds2(Vector2.Zero, Game.Resolution), background); string text = "You escaped!"; float textAlpha = Clamp((EndGameFrame - 8) / 5f, 0, 1); Color textColor = Assets.WinningTextColor.WithAlpha(textAlpha); Vector2 pos = new Vector2((20 - (text.Length + 1) / 2) / 2, 5) * Assets.CellSize; TileEngine.DrawTileString(Assets.FontTiles, text, pos, color: textColor); } if (EndGameFrame > 20) { string text = "\x1A New game"; Vector2 pos = new Vector2((20 - (text.Length + 1) / 2) / 2, 11) * Assets.CellSize; Color color = (State == GameState.Winning) ? Assets.WinningTextColor : Assets.LosingTextColor; TileEngine.DrawTileString(Assets.FontTiles, text, pos, color: color); if (Engine.GetKeyDown(Key.N)) { Restart = true; } } //========================================================================================== // Debug information: //========================================================================================== if (Game.Debug) { foreach (Action action in diagnostics) { action(); } } }