diff --git a/Blocktest/BlocktestGame.cs b/Blocktest/BlocktestGame.cs index 7e08bf9..23078b0 100644 --- a/Blocktest/BlocktestGame.cs +++ b/Blocktest/BlocktestGame.cs @@ -15,6 +15,7 @@ public BlocktestGame() _graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; IsMouseVisible = true; + Window.AllowUserResizing = true; } /// @@ -35,7 +36,7 @@ protected override void LoadContent() protected override void Update(GameTime gameTime) { _currentScene?.Update(gameTime); - + base.Update(gameTime); } diff --git a/Blocktest/Code/Block System/Tilemap.cs b/Blocktest/Code/Block System/Tilemap.cs index 1654cc3..e19989b 100644 --- a/Blocktest/Code/Block System/Tilemap.cs +++ b/Blocktest/Code/Block System/Tilemap.cs @@ -19,43 +19,25 @@ public class Tilemap /// public readonly Vector2Int tilemapSize; /// - /// The size of each cell (in pixels) in the tilemap's grid. - /// - public readonly Vector2Int gridSize = new(8, 8); - /// /// A list of s that specify which blocks should be refreshed when a tile is placed/destroyed. Defaults to the changed block and all cardinal directions. /// - private readonly List adjacencies = new() { Vector2Int.Zero, Vector2Int.Up, Vector2Int.Down, Vector2Int.Left, Vector2Int.Right }; + private static readonly List adjacencies = new() { Vector2Int.Zero, Vector2Int.Up, Vector2Int.Down, Vector2Int.Left, Vector2Int.Right }; + private readonly Camera _camera; /// /// Creates a . /// /// The width of the tilemap in tiles. /// The height of the tilemap in tiles. - public Tilemap(int sizeX, int sizeY) + /// The camera to render tiles on + public Tilemap(int sizeX, int sizeY, Camera camera) { + _camera = camera; tilemapSize = new(sizeX, sizeY); tileGrid = new Tile[sizeX, sizeY]; } - /// - /// Called from the main draw loop, calls on each tile in the tilemap. - /// - /// The spritebatch to draw the tilemap tiles' sprite on. - public void Draw(SpriteBatch spriteBatch) - { - foreach (Tile tile in allTiles) { - tile.Draw(spriteBatch); - } - } - - /// - /// Sets a Tile at the given XYZ coordinates of a cell in the tile map to a specific type. - /// - /// Location the new Block will be placed. - /// Block type to be placed in the cell. - public Tile SetBlock(Vector2Int location, Block newBlock) => SetTile(location, new Tile(newBlock, location)); /// /// Sets a Tile at the given XYZ coordinates of a cell in the tile map to a specific type. /// @@ -66,12 +48,14 @@ public Tile SetTile(Vector2Int location, Tile newTile) Tile oldTile = GetTile(location); if (oldTile != null) { allTiles.Remove(oldTile); + _camera.RenderedComponents.Remove(oldTile.Renderable); } tileGrid[location.X, location.Y] = newTile; if (newTile != null) { allTiles.Add(newTile); + _camera.RenderedComponents.Add(newTile.Renderable); } foreach (Vector2Int dir in adjacencies) { @@ -93,37 +77,13 @@ public Tile SetTile(Vector2Int location, Tile newTile) /// /// Location of the Tile on the Tilemap to check. /// placed at the cell. - public Tile? GetTile(Vector2Int location) => GetTile(location.X, location.Y); - - /// - /// Gets the at a specific location on a . - /// - /// X position of the Tile on the Tilemap to check. - /// Y position of the Tile on the Tilemap to check. - /// placed at the cell. - public Tile? GetTile(int x, int y) { - if (x < 0 || y < 0 || x >= tilemapSize.X || y >= tilemapSize.Y) { + public Tile? GetTile(Vector2Int location) { + if (location.X < 0 || location.Y < 0 || location.X >= tilemapSize.X || location.Y >= tilemapSize.Y) { return null; } - return tileGrid[x, y]; + return tileGrid[location.X, location.Y]; } - /// - /// Gets the at a specific location on a . - /// - /// The subtype of Tile to return. - /// Location of the Tile on the Tilemap to check. - /// of type T placed at the cell. - public T? GetTile(Vector2Int location) where T : Tile => (T?)GetTile(location.X, location.Y); - /// - /// Gets the at a specific location on a . - /// - /// The subtype of Tile to return. - /// X position of the Tile on the Tilemap to check. - /// Y position of the Tile on the Tilemap to check. - /// of type T placed at the cell. - public T? GetTile(int x, int y) where T : Tile => (T?)GetTile(x, y); - /// /// Returns whether there is a at the location specified. /// @@ -150,25 +110,18 @@ public class Tile /// The size of the tile square's edges, in pixels (Default 8) /// protected int size = 8; - /// - /// Color of the tile. - /// - public Color color = Color.White; - /// - /// The rectangle of the tile, used for sprite rendering and collisions. - /// - public Rectangle rectangle; + + public Renderable Renderable; /// /// Creates a . /// /// The type of block the new tile should be. /// The position in a tilemap the tile will be. - public Tile(Block newBlock, Vector2Int position) + public Tile(Block newBlock, Vector2Int position, Layer layer = Layer.ForegroundBlocks) { SourceBlock = newBlock; - sprite = SourceBlock.blockSprite; - rectangle = new Rectangle(Globals.gridSize.X * position.X, Globals.gridSize.Y * position.Y, size, size); // HACK: This can probably be done better + Renderable = new Renderable(new Transform(new Vector2(Globals.gridSize.X * position.X, Globals.gridSize.Y * position.Y)), layer, SourceBlock.blockSprite); } /// @@ -183,10 +136,10 @@ public void UpdateAdjacencies(Vector2Int position, Tilemap tilemap) int bitmask = 0; // Using bitmask smoothing, look it up if (HasSmoothableTile(position + Vector2Int.Up, tilemap)) { - bitmask += 2; + bitmask += 1; } if (HasSmoothableTile(position + Vector2Int.Down, tilemap)) { - bitmask += 1; + bitmask += 2; } if (HasSmoothableTile(position + Vector2Int.Right, tilemap)) { bitmask += 4; @@ -195,7 +148,7 @@ public void UpdateAdjacencies(Vector2Int position, Tilemap tilemap) bitmask += 8; } - sprite = SourceBlock.spriteSheet.OrderedSprites[bitmask]; + Renderable.Appearance = SourceBlock.spriteSheet.OrderedSprites[bitmask]; } /// @@ -211,15 +164,6 @@ private bool HasSmoothableTile(Vector2Int position, Tilemap tilemap) return otherTile != null; } - /// - /// Called from the main draw loop. - /// - /// The spritebatch to draw the tile's sprite on. - public void Draw(SpriteBatch spriteBatch) - { - spriteBatch.Draw(sprite.Texture, new Vector2(rectangle.X, rectangle.Y), sprite.Bounds, Color.White); - } - /// /// If the tile provided is the same type (references the same block) as the current tile. /// diff --git a/Blocktest/Code/BuildSystem.cs b/Blocktest/Code/BuildSystem.cs index 13e6410..ce4de2d 100644 --- a/Blocktest/Code/BuildSystem.cs +++ b/Blocktest/Code/BuildSystem.cs @@ -1,3 +1,5 @@ +using Blocktest.Rendering; + namespace Blocktest { public static class BuildSystem @@ -7,13 +9,6 @@ public static class BuildSystem /// private static readonly int[,,] currentWorld = new int[Globals.maxX, Globals.maxY, 2]; - /// - /// The method called whenever an object is removed. - /// - /// Whether or not the block to be destroyed is in the foreground. - /// The position of the block to destroy (world coords) - //public static void BreakBlockWorld(bool foreground, Vector2 position) => BreakBlockCell(foreground, Globals.foreground.WorldToCell(position)); - /// /// The method called whenever an object is removed. /// @@ -44,14 +39,6 @@ public static void BreakBlockCell(bool foreground, Vector2Int tilePosition) } - /// - /// The method called whenever a block is placed. - /// - /// The block type to place. - /// Whether or not the block should be placed in the foreground. - /// The position of the placed block. (World coords) - //public static void PlaceBlockWorld(Block toPlace, bool foreground, Vector2 position) => PlaceBlockCell(toPlace, foreground, Globals.foreground.WorldToCell(position)); - /// /// The method called whenever a block is placed. /// @@ -60,7 +47,7 @@ public static void BreakBlockCell(bool foreground, Vector2Int tilePosition) /// The position of the placed block. (Grid coords) public static void PlaceBlockCell(Block toPlace, bool foreground, Vector2Int tilePosition) { - Tile newTile = new(toPlace, tilePosition); + Tile newTile = new(toPlace, tilePosition, foreground ? Layer.ForegroundBlocks : Layer.BackgroundBlocks); toPlace.OnPlace(tilePosition, foreground); if (foreground) { @@ -68,19 +55,10 @@ public static void PlaceBlockCell(Block toPlace, bool foreground, Vector2Int til Globals.ForegroundTilemap.SetTile(tilePosition, newTile); currentWorld[tilePosition.X, tilePosition.Y, 0] = toPlace.blockID + 1; } else if (toPlace.canPlaceBackground) { - newTile.color = new Color(0.5f, 0.5f, 0.5f, 1f); + newTile.Renderable.RenderColor = new Color(0.5f, 0.5f, 0.5f, 1f); Globals.BackgroundTilemap.SetTile(tilePosition, newTile); currentWorld[tilePosition.X, tilePosition.Y, 1] = toPlace.blockID + 1; } } - - /// - /// The method called whenever a block is placed. - /// - /// The block type to place. - /// Whether or not the block should be placed in the foreground. - /// The position of the placed block. (Grid coords) - public static void PlaceBlockCell(Block toPlace, bool foreground, Vector2 tilePosition) => PlaceBlockCell(toPlace, foreground, (Vector2Int)tilePosition); - } } \ No newline at end of file diff --git a/Blocktest/Code/Rendering/Camera.cs b/Blocktest/Code/Rendering/Camera.cs new file mode 100644 index 0000000..71eacc8 --- /dev/null +++ b/Blocktest/Code/Rendering/Camera.cs @@ -0,0 +1,62 @@ +using System.Collections; +namespace Blocktest.Rendering; + +public sealed class Camera { + private readonly Color _backgroundColor; + private readonly Vector2 _size; + + public readonly List RenderedComponents = new(); + + public Rectangle RenderLocation; + public readonly RenderTarget2D RenderTarget; + public Vector2 Position; + + public Camera(Vector2 position, Vector2 size, GraphicsDevice graphicsDevice, Color? backgroundColor = null) { + Position = position; + _size = size; + _backgroundColor = backgroundColor ?? Color.CornflowerBlue; + RenderTarget = new RenderTarget2D(graphicsDevice, (int)size.X, (int)size.Y, false, SurfaceFormat.Color, DepthFormat.None, 0, RenderTargetUsage.DiscardContents); + } + + public void Draw(GraphicsDevice graphics, SpriteBatch spriteBatch) { + graphics.SetRenderTarget(RenderTarget); + graphics.Clear(_backgroundColor); + + spriteBatch.Begin(); + + foreach (Renderable component in RenderedComponents) { + if (component.Appearance == null) { + continue; + } + + Vector2 worldPosition = component.Transform.Position; + Vector2 cameraPosition = worldPosition - Position; + + if (worldPosition.X + component.Appearance.Bounds.Width < Position.X && + worldPosition.X > Position.X + _size.X && + worldPosition.Y + component.Appearance.Bounds.Height < Position.Y && + worldPosition.Y > Position.Y + _size.Y) { + continue; + } + + Vector2 flippedPosition = new(cameraPosition.X, RenderTarget.Height - cameraPosition.Y - component.Appearance.Bounds.Height); + + Rectangle positionBounds = new((int)flippedPosition.X, (int)flippedPosition.Y, (int)(component.Appearance.Bounds.Width * component.Transform.Scale.X), + (int)(component.Appearance.Bounds.Height * component.Transform.Scale.Y)); + + spriteBatch.Draw(component.Appearance.Texture, positionBounds, component.Appearance.Bounds, + component.RenderColor, component.Transform.Rotation, component.Transform.Origin, SpriteEffects.None, (float)component.Layer / EnumCount); + } + + spriteBatch.End(); + + graphics.SetRenderTarget(null); + } + + private static readonly int EnumCount = Enum.GetValues(typeof(Layer)).Length; + + public Vector2 CameraToWorldPos(Vector2 mouseState) { + return new((mouseState.X - RenderLocation.X) / RenderLocation.Width * RenderTarget.Width + Position.X, Position.Y + RenderTarget.Height - + (mouseState.Y - RenderLocation.Y) / RenderLocation.Height * RenderTarget.Height); + } +} \ No newline at end of file diff --git a/Blocktest/Code/Rendering/Renderable.cs b/Blocktest/Code/Rendering/Renderable.cs new file mode 100644 index 0000000..0b59fdf --- /dev/null +++ b/Blocktest/Code/Rendering/Renderable.cs @@ -0,0 +1,32 @@ +namespace Blocktest.Rendering; + +public enum Layer { + Top = 0, + Player = 1, + Default = 2, + ForegroundBlocks = 3, + BackgroundBlocks = 4 +} + +public sealed class Renderable { + public readonly Transform Transform; + public Drawable? Appearance; + public Color RenderColor; + public Layer Layer; + + public Renderable(Transform transform, Layer layer = Layer.Default, Drawable? appearance = null, Color? renderColor = null) { + Transform = transform; + Layer = layer; + Appearance = appearance; + RenderColor = renderColor ?? Color.White; + } + + public void Draw(SpriteBatch spriteBatch, Vector2 cameraPosition) { + if (Appearance == null) { + return; + } + + spriteBatch.Draw(Appearance.Texture, Transform.Position - cameraPosition, Appearance.Bounds, RenderColor, Transform.Rotation, Transform.Origin, Transform.Scale, + SpriteEffects.None, 0); + } +} \ No newline at end of file diff --git a/Blocktest/Code/Rendering/Transform.cs b/Blocktest/Code/Rendering/Transform.cs new file mode 100644 index 0000000..c2b5050 --- /dev/null +++ b/Blocktest/Code/Rendering/Transform.cs @@ -0,0 +1,15 @@ +namespace Blocktest.Rendering; + +public sealed class Transform { + public Vector2 Position; + public float Rotation; + public Vector2 Scale; + public Vector2 Origin; + + public Transform(Vector2 position, Vector2? scale = null, float rotation = 0, Vector2? origin = null) { + Position = position; + Scale = scale ?? Vector2.One; + Rotation = rotation; + Origin = origin ?? Vector2.Zero; + } +} \ No newline at end of file diff --git a/Blocktest/Code/Scenes/GameScene.cs b/Blocktest/Code/Scenes/GameScene.cs index cee338b..34e7f0b 100644 --- a/Blocktest/Code/Scenes/GameScene.cs +++ b/Blocktest/Code/Scenes/GameScene.cs @@ -1,3 +1,4 @@ +using Blocktest.Rendering; using Microsoft.Xna.Framework.Input; namespace Blocktest.Scenes; @@ -9,14 +10,18 @@ public class GameScene : Scene { private bool latchBlockSelect = false; //same but for block selection bool buildMode = true; //true for build, false for destroy private int blockSelected = 0; //ID of the block to place + private Camera _camera; public void Update(GameTime gameTime) { - if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) { + MouseState mouseState = Mouse.GetState(); + KeyboardState keyState = Keyboard.GetState(); + + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || keyState.IsKeyDown(Keys.Escape)) { _game.Exit(); } //press E to toggle build/destroy - if (Keyboard.GetState().IsKeyUp(Keys.E)) + if (keyState.IsKeyUp(Keys.E)) { latch = false; } @@ -26,11 +31,8 @@ public void Update(GameTime gameTime) { latch = true; } - //for block placement - MouseState currentState = Mouse.GetState(); - //Q changes which block you have selected - if (Keyboard.GetState().IsKeyUp(Keys.Q)) + if (keyState.IsKeyUp(Keys.Q)) { latchBlockSelect = false; } @@ -45,66 +47,124 @@ public void Update(GameTime gameTime) { latchBlockSelect = true; } + var moveValue = 2.5f; + if (keyState.IsKeyDown(Keys.LeftShift) || keyState.IsKeyDown(Keys.RightShift)) { + moveValue *= 4; + } + + if (keyState.IsKeyDown(Keys.A)) { + _camera.Position.X -= moveValue; + } else if (keyState.IsKeyDown(Keys.D)) { + _camera.Position.X += moveValue; + } + + if (keyState.IsKeyDown(Keys.W)) { + _camera.Position.Y += moveValue; + } else if (keyState.IsKeyDown(Keys.S)) { + _camera.Position.Y -= moveValue; + } + + if (!_camera.RenderLocation.Contains(mouseState.Position)) { + return; + } + + var mousePos = _camera.CameraToWorldPos(new(mouseState.X, mouseState.Y)); //build and destroy mode if (buildMode) { - if(currentState.LeftButton == ButtonState.Pressed) + if(mouseState.LeftButton == ButtonState.Pressed) { BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[blockSelected], true, - new Vector2Int(MathHelper.Clamp(currentState.X / Globals.gridSize.X, 0, Globals.maxX), - MathHelper.Clamp(currentState.Y / Globals.gridSize.Y, 0, Globals.maxY))); - } else if (currentState.RightButton == ButtonState.Pressed) { + new Vector2Int(MathHelper.Clamp(mousePos.X / Globals.gridSize.X, 0, Globals.maxX), + MathHelper.Clamp(mousePos.Y / Globals.gridSize.Y, 0, Globals.maxY))); + } else if (mouseState.RightButton == ButtonState.Pressed) { BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[blockSelected], false, - new Vector2Int(MathHelper.Clamp(currentState.X / Globals.gridSize.X, 0, Globals.maxX), - MathHelper.Clamp(currentState.Y / Globals.gridSize.Y, 0, Globals.maxY))); + new Vector2Int(MathHelper.Clamp(mousePos.X / Globals.gridSize.X, 0, Globals.maxX), + MathHelper.Clamp(mousePos.Y / Globals.gridSize.Y, 0, Globals.maxY))); } } else { - if(currentState.LeftButton == ButtonState.Pressed) + if(mouseState.LeftButton == ButtonState.Pressed) { BuildSystem.BreakBlockCell( true, - new Vector2Int(MathHelper.Clamp(currentState.X / Globals.gridSize.X, 0, Globals.maxX), - MathHelper.Clamp(currentState.Y / Globals.gridSize.Y, 0, Globals.maxY))); - } else if (currentState.RightButton == ButtonState.Pressed) { + new Vector2Int(MathHelper.Clamp(mousePos.X / Globals.gridSize.X, 0, Globals.maxX), + MathHelper.Clamp(mousePos.Y / Globals.gridSize.Y, 0, Globals.maxY))); + } else if (mouseState.RightButton == ButtonState.Pressed) { BuildSystem.BreakBlockCell( false, - new Vector2Int(MathHelper.Clamp(currentState.X / Globals.gridSize.X, 0, Globals.maxX), - MathHelper.Clamp(currentState.Y / Globals.gridSize.Y, 0, Globals.maxY))); + new Vector2Int(MathHelper.Clamp(mousePos.X / Globals.gridSize.X, 0, Globals.maxX), + MathHelper.Clamp(mousePos.Y / Globals.gridSize.Y, 0, Globals.maxY))); } - } - + } } public void Draw(GameTime gameTime, GraphicsDevice graphicsDevice) { - graphicsDevice.Clear(Color.CornflowerBlue); - _spriteBatch.Begin(); - Globals.BackgroundTilemap.Draw(_spriteBatch); - Globals.ForegroundTilemap.Draw(_spriteBatch); - // placement preview - if (buildMode) - _spriteBatch.Draw(BlockManager.AllBlocks[blockSelected].blockSprite.Texture, - new Vector2Int(Mouse.GetState().X - (Mouse.GetState().X % 8), - (Mouse.GetState().Y - Mouse.GetState().Y % 8)), - new Rectangle(1, 1, 10, 10), Color.DimGray); + + _camera.Draw(graphicsDevice, _spriteBatch); + + const bool pixelPerfect = false; + + var destinationRectangle = pixelPerfect ? GetPixelPerfectRect() : GetFitRect(); + _camera.RenderLocation = destinationRectangle; + graphicsDevice.Clear(Color.DarkGray); + + _spriteBatch.Begin(samplerState: pixelPerfect ? SamplerState.PointClamp : null); + _spriteBatch.Draw(_camera.RenderTarget, destinationRectangle, Color.White); _spriteBatch.End(); } + private Rectangle GetPixelPerfectRect() { + int multiplier = int.Min(_game.GraphicsDevice.Viewport.Height / _camera.RenderTarget.Height, + _game.GraphicsDevice.Viewport.Width / _camera.RenderTarget.Width); + + int width = _camera.RenderTarget.Width * multiplier; + int height = _camera.RenderTarget.Height * multiplier; + + int x = (_game.GraphicsDevice.Viewport.Width - width) / 2; + int y = (_game.GraphicsDevice.Viewport.Height - height) / 2; + + return new Rectangle(x, y, width, height); + } + + private Rectangle GetFitRect() { + float aspectRatio = (float)_game.GraphicsDevice.Viewport.Width / _game.GraphicsDevice.Viewport.Height; + float renderTargetAspectRatio = (float)_camera.RenderTarget.Width / _camera.RenderTarget.Height; + + int width, height; + if (aspectRatio > renderTargetAspectRatio) { + width = (int)(_game.GraphicsDevice.Viewport.Height * renderTargetAspectRatio); + height = _game.GraphicsDevice.Viewport.Height; + } + else { + width = _game.GraphicsDevice.Viewport.Width; + height = (int)(_game.GraphicsDevice.Viewport.Width / renderTargetAspectRatio); + } + + int x = (_game.GraphicsDevice.Viewport.Width - width) / 2; + int y = (_game.GraphicsDevice.Viewport.Height - height) / 2; + + return new Rectangle(x, y, width, height); + } + + + public GameScene(BlocktestGame game) { _spriteBatch = new SpriteBatch(game.GraphicsDevice); _game = game; + _camera = new Camera(new Vector2(0, 0), new Vector2(512, 256), _game.GraphicsDevice); - Globals.BackgroundTilemap = new Tilemap(Globals.maxX, Globals.maxY); - Globals.ForegroundTilemap = new Tilemap(Globals.maxX, Globals.maxY); + Globals.BackgroundTilemap = new Tilemap(Globals.maxX, Globals.maxY, _camera); + Globals.ForegroundTilemap = new Tilemap(Globals.maxX, Globals.maxY, _camera); for (int i = 0; i < Globals.maxX; i++) { - BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[2], true, new Vector2Int(i, 59)); - BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[0], true, new Vector2Int(i, 58)); - BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[0], true, new Vector2Int(i, 57)); - BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[0], true, new Vector2Int(i, 56)); - BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[0], true, new Vector2Int(i, 55)); - BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[1], true, new Vector2Int(i, 54)); + BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[1], true, new Vector2Int(i, 5)); + BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[0], true, new Vector2Int(i, 4)); + BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[0], true, new Vector2Int(i, 3)); + BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[0], true, new Vector2Int(i, 2)); + BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[0], true, new Vector2Int(i, 1)); + BuildSystem.PlaceBlockCell(BlockManager.AllBlocks[2], true, new Vector2Int(i, 0)); } } } \ No newline at end of file