XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

AshfallFrontier

【Unity】灰烬边境,一款生存策略游戏

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.Generic;
using System.Linq;
using Ashfall.Core;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Rendering.Universal;

namespace Ashfall.Runtime
{
    public sealed class WorldView : MonoBehaviour
    {
        private GameSession session;
        private SpriteLibrary art;
        private Camera cameraView;
        private Light2D ambient, playerLight;
        private Transform root;
        private readonly Dictionary<string, SpriteRenderer> renderers = new Dictionary<string, SpriteRenderer>();
        private readonly HashSet<string> visible = new HashSet<string>();
        private readonly Dictionary<string, V2> displayPositions = new Dictionary<string, V2>();
        private FloorPhysics floors;
        private string previousSpace = "";
        private int previousFloor = -1;
        private float zoom = 12;
        public bool Night { get; set; }
        public string ViewedSpace { get; private set; } = "world";
        public int ViewedFloor { get; private set; }
        public V2 MouseGround { get; private set; }
        public Camera Camera => cameraView;
        public VehicleState ControlledVehicle => session.LocalPlayer == null ? null :
            !session.IsAuthority && session.PredictedVehicle != null ? session.PredictedVehicle : session.Simulation?.StationVehicle(session.LocalPlayer.stationId);

        public void Initialize(GameSession value)
        {
            session = value; art = new SpriteLibrary();
            root = new GameObject("Visible space sprites").transform; root.SetParent(transform);
            cameraView = Camera.main;
            if (cameraView == null) { var obj = new GameObject("Main Camera"); obj.tag = "MainCamera"; cameraView = obj.AddComponent<Camera>(); obj.AddComponent<AudioListener>(); }
            cameraView.orthographic = true; cameraView.orthographicSize = zoom; cameraView.transform.position = new Vector3(0, 0, -20);
            cameraView.clearFlags = CameraClearFlags.SolidColor; cameraView.backgroundColor = new Color(.075f, .085f, .075f);
            cameraView.nearClipPlane = .1f; cameraView.farClipPlane = 100;
            ambient = new GameObject("Environment light").AddComponent<Light2D>(); ambient.transform.SetParent(transform);
            ambient.lightType = Light2D.LightType.Global; ambient.intensity = .92f;
            playerLight = new GameObject("Player worklight").AddComponent<Light2D>(); playerLight.transform.SetParent(transform);
            playerLight.lightType = Light2D.LightType.Point; playerLight.pointLightOuterRadius = 10; playerLight.pointLightInnerRadius = 2;
            playerLight.intensity = .3f; playerLight.color = new Color(.77f, .88f, .95f);
            floors = new FloorPhysics();
        }
        private void Update()
        {
            if (cameraView == null) return;
            var mouse = Mouse.current;
            if (mouse != null)
            {
                var screen = cameraView.ScreenToWorldPoint(mouse.position.ReadValue()); MouseGround = Projection.ToGround(new V2(screen.x, screen.y));
                if (session.Running && GetComponent<GameHud>()?.CapturesPointer != true)
                    zoom = Mathf.Clamp(zoom - mouse.scroll.ReadValue().y * .006f, 7, 28);
            }
        }
        private void LateUpdate()
        {
            if (art == null) return; visible.Clear();
            var world = session.World; var player = session.LocalPlayer;
            if (world == null || player == null || !session.Running)
            { HideUnused(); return; }
            var v = ControlledVehicle;
            ViewedSpace = v == null ? player.location.spaceId : "world";
            ViewedFloor = v == null ? player.location.floor : 0;
            var space = world.Space(ViewedSpace); if (space == null) return;
            bool changed = previousSpace != ViewedSpace || previousFloor != ViewedFloor;
            if (changed) displayPositions.Clear();
            previousSpace = ViewedSpace; previousFloor = ViewedFloor;
            var predicted = !session.IsAuthority && session.PredictedPlayer != null ? session.PredictedPlayer : player;
            V2 focus = v == null ? predicted.location.position : v.position;
            V2 projected = Projection.ToScreen(focus, v == null ? predicted.location.height : v.Altitude);
            if (v == null && ViewedSpace != "world")
            {
                var f = world.Floor(player.location);
                projected.x = Mathf.Clamp(projected.x, f.bounds.center.x - 3, f.bounds.center.x + 3);
                projected.y = Mathf.Clamp(projected.y, f.bounds.center.y - 2, f.bounds.center.y + 2);
            }
            else if (v == null) projected += (Projection.ToScreen(MouseGround) - projected) * .1f;
            var target = new Vector3(projected.x, projected.y, -20);
            cameraView.transform.position = changed ? target : Vector3.Lerp(cameraView.transform.position, target, 1 - Mathf.Exp(-Time.unscaledDeltaTime * 8));
            float targetZoom = v == null ? ViewedSpace == "world" ? zoom : Mathf.Min(zoom, 7.5f) : Mathf.Max(zoom, v.flying ? 22 : 16);
            cameraView.orthographicSize = Mathf.Lerp(cameraView.orthographicSize, targetZoom, 1 - Mathf.Exp(-Time.unscaledDeltaTime * 6));
            ambient.intensity = Night ? .27f : ViewedSpace == "world" ? .92f : .62f;
            ambient.color = Night ? new Color(.57f, .67f, .83f) : new Color(.96f, .94f, .86f);
            playerLight.transform.position = new Vector3(projected.x, projected.y, -1);
            playerLight.intensity = Night || ViewedSpace != "world" ? 1.05f : .2f;
            if (ViewedSpace == "world") DrawExterior(world, focus);
            else DrawInterior(world, space, ViewedFloor);
            DrawEntities(world, changed);
            DrawBuildPreview(); HideUnused();
            floors.Synchronize(world, session.Revision);
            floors.Simulate(Time.unscaledDeltaTime);
        }
        private SpriteRenderer Put(string id, Sprite sprite, V2 position, V2 size, Color color, int order, bool unlit = false)
        {
            visible.Add(id);
            if (!renderers.TryGetValue(id, out var renderer))
            {
                var obj = new GameObject(id); obj.transform.SetParent(root); renderer = obj.AddComponent<SpriteRenderer>(); renderers[id] = renderer;
            }
            renderer.gameObject.SetActive(true); renderer.sprite = sprite; renderer.sharedMaterial = unlit ? art.Unlit : art.Lit;
            renderer.color = color; renderer.sortingOrder = Mathf.Clamp(order, -32000, 32000);
            renderer.transform.position = new Vector3(position.x, position.y, 0); renderer.transform.rotation = Quaternion.identity;
            renderer.transform.localScale = new Vector3(size.x / Mathf.Max(.001f, sprite.bounds.size.x), size.y / Mathf.Max(.001f, sprite.bounds.size.y), 1);
            return renderer;
        }
        private V2 Interpolate(string key, V2 target, bool immediate = false)
        {
            if (session.IsAuthority || immediate || !displayPositions.TryGetValue(key, out var previous) || V2.Distance(previous, target) > 8)
                return displayPositions[key] = target;
            return displayPositions[key] = V2.Lerp(previous, target, 1 - Mathf.Exp(-Time.unscaledDeltaTime * 15));
        }
        private void Rect(string id, V2 at, V2 size, Color color, int order, bool unlit = false)
            => Put(id, art.Get("white"), Projection.ToScreen(at), new V2(size.x, size.y * Projection.GroundScale), color, order, unlit);
        private void Tile(string id, V2 at, V2 size, Color color, int order, bool metal)
            => Put(id, art.Get(metal ? "metal" : "soil"), Projection.ToScreen(at), new V2(size.x, size.y * Projection.GroundScale), color, order);
        private static int Depth(V2 ground) => 10000 - Mathf.RoundToInt(ground.y * 30);
        private void Shadow(string id, V2 at, V2 size, float alpha = .8f)
            => Put(id, art.Get("shadow"), Projection.ToScreen(at), new V2(size.x, size.y * Projection.GroundScale), new Color(1, 1, 1, alpha), -1000, true);
        private void DrawExterior(WorldState world, V2 focus)
        {
            int cx = Mathf.RoundToInt(focus.x / 4), cy = Mathf.RoundToInt(focus.y / 4);
            for (int x = cx - 15; x <= cx + 15; x++) for (int y = cy - 12; y <= cy + 12; y++)
            {
                float variation = Mathf.PerlinNoise((x + 121) * .13f, (y + 77) * .13f);
                Tile("ground." + x + "." + y, new V2(x * 4, y * 4), new V2(4.02f, 4.02f),
                    Color.Lerp(new Color(.19f, .205f, .17f), new Color(.32f, .29f, .23f), variation), -30000, false);
            }
            foreach (var s in world.spaces.Where(s => s.kind == SpaceKind.Building))
            {
                bool built = s.id == "base"; V2 size = built ? new V2(18, 18) : new V2(16, 14); V2 center = s.worldAnchor;
                Shadow(s.id + ".shadow", center + new V2(2, -1.5f), size + new V2(3, 3));
                Tile(s.id + ".roof", center, size, built ? new Color(.32f, .36f, .35f) : new Color(.43f, .45f, .4f), Depth(center), true);
                Rect(s.id + ".facade", center + new V2(0, -size.y / 2), new V2(size.x, 1.5f), new Color(.12f, .16f, .17f), Depth(center) + 1);
                Rect(s.id + ".door", center + new V2(0, -size.y / 2), new V2(2, 1.6f), new Color(.07f, .09f, .09f), Depth(center) + 2);
                Rect(s.id + ".light", center + new V2(0, -size.y / 2 + 1), new V2(2.1f, .12f), new Color(.42f, .73f, .69f), Depth(center) + 3, true);
                for (int i = -1; i <= 1; i++) Rect(s.id + ".vent" + i, center + new V2(i * 4, 2), new V2(2, 4), new Color(.23f, .27f, .27f), Depth(center) + 2);
            }
            foreach (var authority in world.vehicles.Where(v => v.dockedTo == ""))
            {
                var v = !session.IsAuthority && session.PredictedVehicle?.id == authority.id ? session.PredictedVehicle : authority;
                V2 at = Interpolate("vehicle." + v.id, v.position, session.PredictedVehicle?.id == v.id);
                string asset = v.flying ? "ark" : v.family == Family.Mechanical ? "crawler" : "beast";
                float altitude = v.Altitude;
                Shadow(v.id + ".shadow", at + new V2(altitude * .28f, -altitude * .2f), new V2(v.width * 1.2f, v.length * 1.1f), Mathf.Clamp01(.9f - altitude * .012f));
                int direction = Mathf.RoundToInt(Mathf.Repeat(-v.heading, 360) / 90) % 4;
                bool side = direction % 2 == 1;
                Put(v.id, art.Direction(asset, v.heading), Projection.ToScreen(at, altitude),
                    new V2(side ? v.length * 1.25f : v.width * 1.45f, side ? v.width * 1.45f : v.length * 1.04f), Color.white, Depth(v.position) + (altitude > 0 ? 5000 : 0));
                foreach (var hp in v.hardpoints)
                {
                    V2 mount = at + new V2(hp.group == 0 ? -v.width * .18f : v.width * .18f, v.length * .12f).Rotate(v.heading);
                    var turret = Put(v.id + "." + hp.id, art.Get("prop0"), Projection.ToScreen(mount, altitude + 1), new V2(2.2f, 2.2f),
                        v.family == Family.Mechanical ? Color.white : new Color(.68f, .77f, .51f), Depth(v.position) + 2 + (altitude > 0 ? 5000 : 0));
                    turret.transform.rotation = Quaternion.Euler(0, 0, hp.aimHeading);
                }
                if (v.doorOpen) Rect(v.id + ".entry", v.DoorPosition, new V2(2, .4f), new Color(.33f, .82f, .72f, .85f), 18000, true);
            }
            foreach (var wall in world.Space("world").floors[0].walls)
            {
                Shadow(wall.id + ".shadow", wall.bounds.center + new V2(.8f, -.5f), wall.bounds.size + new V2(1, 1));
                Put(wall.id, art.Get("extra3"), Projection.ToScreen(wall.bounds.center), new V2(wall.bounds.size.x * 1.4f, wall.bounds.size.y * Projection.GroundScale), new Color(.72f, .76f, .73f), Depth(wall.bounds.center));
            }
        }
        private void DrawInterior(WorldState world, SpaceState space, int floorIndex)
        {
            var floor = space.floors.Find(f => f.index == floorIndex); if (floor == null) return;
            int sx = Mathf.FloorToInt(floor.bounds.size.x / 2), sy = Mathf.FloorToInt(floor.bounds.size.y / 2);
            for (int x = -sx; x <= sx; x++) for (int y = -sy; y <= sy; y++)
            {
                if (floor.playerBuilt && !world.pieces.Any(b => b.kind == PieceKind.Floor && b.location.spaceId == space.id && b.location.floor == floorIndex && Mathf.Abs(b.location.position.x - x) < .1f && Mathf.Abs(b.location.position.y - y) < .1f)) continue;
                bool bio = space.family == Family.Biological;
                Tile("floor." + x + "." + y, new V2(x, y), new V2(1, 1), bio ? new Color(.32f, .27f, .22f) : new Color(.36f, .4f, .4f), -20000, !bio);
            }
            Rect("wall.back", new V2(0, sy), new V2(sx * 2 + 1, 1.3f), new Color(.18f, .23f, .25f), 9000);
            Rect("wall.left", new V2(-sx, 0), new V2(.35f, sy * 2), new Color(.17f, .21f, .22f), 9000);
            Rect("wall.right", new V2(sx, 0), new V2(.35f, sy * 2), new Color(.17f, .21f, .22f), 9000);
            // Foreground wall is drawn at cutaway height only; no other floor is disabled globally.
            Rect("wall.front", new V2(0, -sy), new V2(sx * 2, .28f), new Color(.27f, .36f, .36f, .5f), 16000);
            foreach (var f in world.facilities.Where(f => f.location.spaceId == space.id && f.location.floor == floorIndex))
            {
                string key = f.kind == FacilityKind.Storage ? "prop2" : f.kind == FacilityKind.Generator ? "extra1" : f.family == Family.Biological ? "extra0" : "prop1";
                Shadow(f.id + ".shadow", f.location.position, new V2(2.7f, 1.8f));
                Put(f.id, art.Get(key), Projection.ToScreen(f.location.position) + new V2(0, .5f), new V2(2.4f, 2.4f), f.powered ? Color.white : new Color(.5f, .5f, .5f), Depth(f.location.position));
                Rect(f.id + ".state", f.location.position + new V2(0, -.8f), new V2(.9f, .08f), f.powered ? new Color(.3f, .8f, .65f) : new Color(.9f, .37f, .17f), 17000, true);
            }
            foreach (var vehicle in world.vehicles) foreach (var station in vehicle.stations.Where(s => s.location.spaceId == space.id && s.location.floor == floorIndex))
            {
                Put(station.id, art.Get("prop1"), Projection.ToScreen(station.location.position) + new V2(0, .55f), new V2(2.2f, 2.2f), new Color(.76f, .92f, .98f), Depth(station.location.position));
                Rect(station.id + ".state", station.location.position + new V2(0, -.9f), new V2(1.2f, .13f), station.playerId == "" ? new Color(.28f, .71f, .8f) : new Color(.97f, .74f, .3f), 17000, true);
            }
            foreach (var portal in world.portals.Where(p => p.from.spaceId == space.id && p.from.floor == floorIndex))
            {
                if (portal.stairs)
                    Put(portal.id, art.Get("extra2"), Projection.ToScreen(portal.from.position) + new V2(0, .5f), new V2(2.1f, 3), new Color(.9f, .93f, .9f), 9500);
                Rect(portal.id + ".entry", portal.from.position, new V2(1.4f, .32f), new Color(.33f, .76f, .74f, .8f), 16000, true);
            }
            foreach (var b in world.pieces.Where(b => b.location.spaceId == space.id && b.location.floor == floorIndex))
            {
                if (b.kind == PieceKind.Wall)
                    Rect(b.id, b.location.position, b.rotation % 2 == 0 ? new V2(1, .4f) : new V2(.4f, 1), new Color(.21f, .27f, .29f), Depth(b.location.position));
                else if (b.kind == PieceKind.Door) Rect(b.id, b.location.position, new V2(.8f, .2f), new Color(.38f, .65f, .62f), Depth(b.location.position));
                else if (b.kind == PieceKind.Turret) Put(b.id, art.Get("prop0"), Projection.ToScreen(b.location.position), new V2(2, 2), Color.white, Depth(b.location.position));
            }
        }
        private void DrawEntities(WorldState world, bool changed)
        {
            foreach (var node in world.resources.Where(x => x.location.spaceId == ViewedSpace && x.location.floor == ViewedFloor))
            {
                Put(node.id, art.Get(node.itemId == "biomass" ? "prop3" : "prop2"), Projection.ToScreen(node.location.position), new V2(1.2f, 1.2f),
                    node.itemId == "sample" ? new Color(.45f, .85f, .97f) : Color.white, Depth(node.location.position));
            }
            foreach (var e in world.enemies.Where(x => x.location.spaceId == ViewedSpace && x.location.floor == ViewedFloor))
            {
                float size = e.boss ? 4 : 1.8f;
                V2 at = Interpolate(e.id, e.location.position);
                Shadow(e.id + ".shadow", at, new V2(size, size * .5f));
                Put(e.id, art.Get("worm2"), Projection.ToScreen(at), new V2(size, size), e.boss ? new Color(.93f, .61f, .41f) : new Color(.85f, .8f, .67f), Depth(at));
                Rect(e.id + ".health", e.location.position + new V2(0, size * .6f), new V2(size * e.health / e.maxHealth, .08f), new Color(.8f, .28f, .16f), 21000, true);
            }
            foreach (var authority in world.players.Where(x => x.connected && x.location.spaceId == ViewedSpace && x.location.floor == ViewedFloor))
            {
                var p = authority.id == session.PlayerId && !session.IsAuthority && session.PredictedPlayer != null ? session.PredictedPlayer : authority;
                bool worm = p.IsWorm; string key = worm ? "worm" : p.species == Species.Human ? "human" : "adult";
                bool underground = p.burrowUntil > world.time;
                V2 at = Interpolate(p.id, p.location.position, p.id == session.PlayerId);
                Shadow(p.id + ".shadow", at, new V2(worm ? 1.2f : 1.4f, .85f));
                float bob = worm ? Mathf.Sin(Time.time * 8 + p.location.position.x) * .04f : 0;
                V2 pos = Projection.ToScreen(at, p.location.height) + new V2(0, bob);
                Put(p.id, art.Direction(key, p.heading), pos, worm ? new V2(1.4f, 1.4f) : new V2(2.35f, 2.35f),
                    underground ? new Color(.55f, .63f, .48f, .28f) : Color.white, Depth(p.location.position) + 1);
                if (p.id == session.PlayerId) Rect(p.id + ".marker", p.location.position + new V2(0, -.4f), new V2(.75f, .06f), new Color(.35f, .91f, .81f), 17000, true);
            }
            foreach (var shot in world.shots.Where(x => x.spaceId == ViewedSpace && x.floor == ViewedFloor))
            {
                V2 a = Projection.ToScreen(shot.from, shot.height), b = Projection.ToScreen(shot.to), delta = b - a;
                var r = Put("shot." + shot.id, art.Get("white"), (a + b) / 2, new V2(delta.Length, .045f),
                    shot.family == Family.Mechanical ? new Color(1, .83f, .48f, .85f) : new Color(.65f, .95f, .39f, .85f), 22000, true);
                r.transform.rotation = Quaternion.Euler(0, 0, Mathf.Atan2(delta.y, delta.x) * Mathf.Rad2Deg);
            }
        }
        private void DrawBuildPreview()
        {
            var hud = GetComponent<GameHud>();
            if (hud == null || !hud.BuildMode || ViewedSpace != "base") return;
            V2 position = new V2(Mathf.Round(MouseGround.x), Mathf.Round(MouseGround.y));
            Rect("build.preview", position, new V2(1, 1), new Color(.35f, .84f, .85f, .38f), 25000, true);
        }
        private void HideUnused()
        {
            foreach (var pair in renderers) if (!visible.Contains(pair.Key)) pair.Value.gameObject.SetActive(false);
            if (renderers.Count < 2400) return;
            foreach (var id in renderers.Keys.Where(k => !visible.Contains(k)).Take(800).ToArray())
            { Destroy(renderers[id].gameObject); renderers.Remove(id); }
        }
        public Vector2 GuiPosition(Location l)
        {
            V2 p = Projection.ToScreen(l.position, l.height);
            var screen = cameraView.WorldToScreenPoint(new Vector3(p.x, p.y, 0)); return new Vector2(screen.x, Screen.height - screen.y);
        }
        public void ReleasePhysics() { floors?.Dispose(); }
        private void OnDestroy() { floors?.Dispose(); art?.Dispose(); }
    }
}