diff --git a/Shared/Camera.cs b/Shared/Camera.cs index 30d9f9e..f81a943 100644 --- a/Shared/Camera.cs +++ b/Shared/Camera.cs @@ -5,13 +5,18 @@ using System; // https://gamasutra.com/blogs/ItayKeren/20150511/243083/Scroll_Back_The_Theory_and_Practice_of_Cameras_in_SideScrollers.php namespace SemiColinGames { class Camera { - private Rectangle bbox = new Rectangle(0, 0, 1920 / 4, 1080 / 4); + // Screen size in pixels is 1920x1080 divided by 4. + private Rectangle bbox = new Rectangle(0, 0, 480, 270); public int Width { get => bbox.Width; } public int Height { get => bbox.Height; } public int Left { get => bbox.Left; } public int Top { get => bbox.Top; } + public Matrix Projection { + get => Matrix.CreateOrthographicOffCenter(Left, Left + Width, Height, 0, -1, 1); + } + public void Update(Point player, int worldWidth) { int diff = player.X - bbox.Center.X; if (Math.Abs(diff) > 16) { diff --git a/Shared/Player.cs b/Shared/Player.cs index df22e3c..5636a79 100644 --- a/Shared/Player.cs +++ b/Shared/Player.cs @@ -11,7 +11,7 @@ namespace SemiColinGames { Right = 1 }; - enum Pose { Walking, Standing, Crouching, Stretching, SwordSwing, Jumping }; + public enum Pose { Walking, Standing, Crouching, Stretching, SwordSwing, Jumping }; private const int moveSpeed = 180; private const int jumpSpeed = -600; @@ -46,6 +46,15 @@ namespace SemiColinGames { this.texture = texture; } + // TODO: just make Facing an int. + public int GetFacing { + get { return (int) facing; } + } + + public Pose GetPose { + get { return pose; } + } + public Point Position { get { return position; } } public void Update(float modelTime, History input, AABB[] collisionTargets) { @@ -147,102 +156,15 @@ namespace SemiColinGames { } else { pose = Pose.Standing; } - - DrawSightLines(collisionTargets); - } - - Vector2 Rotate(Vector2 point, float angle) { - float cos = FMath.Cos(angle); - float sin = FMath.Sin(angle); - return new Vector2( - point.X * cos - point.Y * sin, - point.Y * cos + point.X * sin); } - bool PointInCone( - float visionRangeSq, float fovCos, Vector2 eyePos, Vector2 direction, Vector2 test) { - Vector2 delta = Vector2.Subtract(test, eyePos); - if (delta.LengthSquared() > visionRangeSq) { - return false; + public Vector2 EyePosition { + get { + bool walking = pose == Pose.Walking || pose == Pose.Jumping; + Vector2 eyeOffset = walking ? eyeOffsetWalking : eyeOffsetStanding; + return Vector2.Add( + Position.ToVector2(), new Vector2(eyeOffset.X * (int) facing, eyeOffset.Y)); } - float dot = Vector2.Dot(Vector2.Normalize(direction), Vector2.Normalize(delta)); - return dot > fovCos; - } - - void DrawSightLines(AABB[] collisionTargets) { - float fov = FMath.DegToRad(45); - float fovCos = FMath.Cos(fov); - Color color = Color.LightYellow; - - Vector2 eyeOffset = pose == Pose.Walking ? eyeOffsetWalking : eyeOffsetStanding; - Vector2 eyePos = Vector2.Add( - Position.ToVector2(), new Vector2(eyeOffset.X * (int) facing, eyeOffset.Y)); - - float visionRange = 150; - float visionRangeSq = visionRange * visionRange; - Vector2 ray = new Vector2(visionRange * (int) facing, 0); - if (pose == Pose.Stretching) { - ray = Rotate(ray, (int) facing * FMath.DegToRad(-30)); - } - if (pose == Pose.Crouching) { - ray = Rotate(ray, (int) facing * FMath.DegToRad(30)); - } - Vector2 coneBottom = Rotate(ray, fov); - Vector2 coneTop = Rotate(ray, -fov); - - List points = new List(); - List boxes = new List(); - points.Add(Vector2.Add(eyePos, coneBottom)); - points.Add(Vector2.Add(eyePos, coneTop)); - foreach (AABB box in collisionTargets) { - int hitCount = points.Count; - if (PointInCone(visionRangeSq, fovCos, eyePos, ray, box.TopLeft)) { - points.Add(box.TopLeft); - } - if (PointInCone(visionRangeSq, fovCos, eyePos, ray, box.TopRight)) { - points.Add(box.TopRight); - } - if (PointInCone(visionRangeSq, fovCos, eyePos, ray, box.BottomLeft)) { - points.Add(box.BottomLeft); - } - if (PointInCone(visionRangeSq, fovCos, eyePos, ray, box.BottomRight)) { - points.Add(box.BottomRight); - } - if (points.Count > hitCount) { - boxes.Add(box); - Debug.AddRect(box, color); - } - } - - HashSet boxesSeen = new HashSet(); - foreach (Vector2 point in points) { - float minTime = 1; - AABB? closestBox = null; - Vector2 delta = Vector2.Subtract(point, eyePos); - foreach (AABB box in boxes) { - Hit? maybeHit = box.IntersectSegment(eyePos, delta); - if (maybeHit != null) { - float time = FMath.Clamp(maybeHit.Value.Time, 0, 1); - Vector2 target = Vector2.Add(eyePos, Vector2.Multiply(delta, time)); - Debug.AddLine(eyePos, target, color); - if (time < minTime) { - minTime = time; - closestBox = box; - } - } - } - if (closestBox != null) { - boxesSeen.Add(closestBox.Value); - } - } - - foreach (AABB box in boxesSeen) { - Debug.AddRect(box, Color.Orange); - } - - Debug.AddLine(eyePos, Vector2.Add(eyePos, ray), Color.Red); - Debug.AddLine(eyePos, Vector2.Add(eyePos, coneTop), Color.Red); - Debug.AddLine(eyePos, Vector2.Add(eyePos, coneBottom), Color.Red); } // Returns the desired (dx, dy) for the player to move this frame. diff --git a/Shared/SneakGame.cs b/Shared/SneakGame.cs index 8bb5d6b..09e3210 100644 --- a/Shared/SneakGame.cs +++ b/Shared/SneakGame.cs @@ -11,7 +11,10 @@ namespace SemiColinGames { const double TARGET_FRAME_TIME = 1.0 / TARGET_FPS; readonly GraphicsDeviceManager graphics; - RenderTarget2D renderTarget; + RenderTarget2D sceneTarget; + RenderTarget2D lightingTarget; + + BasicEffect lightingEffect; SpriteBatch spriteBatch; SpriteFont font; @@ -54,10 +57,23 @@ namespace SemiColinGames { Debug.Initialize(GraphicsDevice); - renderTarget = new RenderTarget2D( + sceneTarget = new RenderTarget2D( + GraphicsDevice, camera.Width, camera.Height, false /* mipmap */, + GraphicsDevice.PresentationParameters.BackBufferFormat, DepthFormat.Depth24); + lightingTarget = new RenderTarget2D( GraphicsDevice, camera.Width, camera.Height, false /* mipmap */, GraphicsDevice.PresentationParameters.BackBufferFormat, DepthFormat.Depth24); + lightingEffect = new BasicEffect(GraphicsDevice); + lightingEffect.World = Matrix.CreateTranslation(0, 0, 0); + lightingEffect.View = Matrix.CreateLookAt(Vector3.Backward, Vector3.Zero, Vector3.Up); + lightingEffect.VertexColorEnabled = true; + + RasterizerState rasterizerState = new RasterizerState() { + CullMode = CullMode.None + }; + GraphicsDevice.RasterizerState = rasterizerState; + base.Initialize(); } @@ -67,7 +83,7 @@ namespace SemiColinGames { font = Content.Load("font"); player = new Player(Content.Load("Ninja_Female")); - world = new World(Content.Load("grassland"), Levels.DEMO); + world = new World(Content.Load("grassland"), Levels.ONE_ONE); grasslandBg1 = Content.Load("grassland_bg1"); grasslandBg2 = Content.Load("grassland_bg2"); } @@ -131,8 +147,8 @@ namespace SemiColinGames { Debug.SetFpsText(fpsText); - // Draw scene to RenderTarget. - GraphicsDevice.SetRenderTarget(renderTarget); + // Draw scene to sceneTarget. + GraphicsDevice.SetRenderTarget(sceneTarget); GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.LinearWrap, null, null); @@ -148,7 +164,8 @@ namespace SemiColinGames { // Set up transformation matrix for drawing world objects. Matrix transform = Matrix.CreateTranslation(-camera.Left, -camera.Top, 0); - spriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.LinearWrap, null, null, null, transform); + spriteBatch.Begin( + SpriteSortMode.Deferred, null, SamplerState.LinearWrap, null, null, null, transform); // Draw player. player.Draw(spriteBatch); @@ -162,7 +179,13 @@ namespace SemiColinGames { // Aaaaand we're done. spriteBatch.End(); - // Draw RenderTarget to screen. + // Draw lighting to lightingTarget. + GraphicsDevice.SetRenderTarget(lightingTarget); + GraphicsDevice.Clear(new Color(0, 0, 0, 0f)); + lightingEffect.Projection = camera.Projection; + DrawFov(); + + // Draw sceneTarget to screen. GraphicsDevice.SetRenderTarget(null); GraphicsDevice.Clear(Color.CornflowerBlue); if (framesToSuppress == 0) { @@ -171,7 +194,8 @@ namespace SemiColinGames { RasterizerState.CullNone); Rectangle drawRect = new Rectangle( 0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height); - spriteBatch.Draw(renderTarget, drawRect, Color.White); + spriteBatch.Draw(sceneTarget, drawRect, Color.White); + spriteBatch.Draw(lightingTarget, drawRect, Color.White); // Draw debug toasts. Debug.DrawToasts(spriteBatch, font); @@ -182,5 +206,77 @@ namespace SemiColinGames { base.Draw(gameTime); drawTimer.Stop(); } + + private void DrawFov() { + // TODO: DrawIndexedPrimitives + Color color = Color.FromNonPremultiplied(new Vector4(0, 0, 1, 0.6f)); + Vector2 eyePos = player.EyePosition; + int numConePoints = 60; + // TODO: don't new[] every frame. + VertexPositionColor[] conePoints = new VertexPositionColor[numConePoints]; + float visionRange = 150; + float visionRangeSq = visionRange * visionRange; + float fov = FMath.DegToRad(120); + float fovStep = fov / (numConePoints - 1); + + Vector2 ray = new Vector2(visionRange * player.GetFacing, 0); + if (player.GetPose == Player.Pose.Stretching) { + ray = ray.Rotate(player.GetFacing * FMath.DegToRad(-30)); + } + if (player.GetPose == Player.Pose.Crouching) { + ray = ray.Rotate(player.GetFacing * FMath.DegToRad(30)); + } + + for (int i = 0; i < conePoints.Length; i++) { + float angle = -fov / 2 + fovStep * i; + Vector2 rotated = ray.Rotate(angle); + Vector2 closestHit = Vector2.Add(eyePos, rotated); + float hitTime = 1f; + + Vector2 halfTileSize = new Vector2(World.TileSize / 2.0f, World.TileSize / 2.0f); + for (int j = 0; j < world.CollisionTargets.Length; j++) { + AABB box = world.CollisionTargets[j]; + if (Math.Abs(box.Position.X - player.Position.X) > visionRange + halfTileSize.X) { + continue; + } + Vector2 delta = Vector2.Add(halfTileSize, Vector2.Subtract(box.Position, eyePos)); + if (delta.LengthSquared() > visionRangeSq) { + continue; + } + Hit? maybeHit = box.IntersectSegment(eyePos, rotated); + if (maybeHit != null) { + Hit hit = maybeHit.Value; + if (hit.Time < hitTime) { + hitTime = hit.Time; + closestHit = hit.Position; + } + } + } + + float tint = 0.6f - hitTime / 2; + Color tinted = Color.FromNonPremultiplied(new Vector4(0, 0, 1, tint)); + conePoints[i] = new VertexPositionColor(new Vector3(closestHit, 0), tinted); + } + + // TODO: don't new[] every frame. + VertexPositionColor[] vertices = new VertexPositionColor[numConePoints * 3]; + VertexPositionColor eyeVertex = new VertexPositionColor(new Vector3(eyePos, 0), color); + for (int i = 0; i < numConePoints - 1; i++) { + vertices[i * 3] = eyeVertex; + vertices[i * 3 + 1] = conePoints[i]; + vertices[i * 3 + 2] = conePoints[i + 1]; + } + + VertexBuffer vertexBuffer = new VertexBuffer( + GraphicsDevice, typeof(VertexPositionColor), vertices.Length, BufferUsage.WriteOnly); + vertexBuffer.SetData(vertices); + + GraphicsDevice.SetVertexBuffer(vertexBuffer); + + foreach (EffectPass pass in lightingEffect.CurrentTechnique.Passes) { + pass.Apply(); + GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, vertices.Length / 3); + } + } } }