Per-map parallax support (#9786)

* Per-map parallax support

* Comments for future sloth

* c

* bet

* Fix exception

* VV support

* Fix parallax

* mem

* weightless sounds

* Gravity stuff

* placeholder coz im too lazy to stash don't @ me son

* decent clouds

* sky

* Fast parallax

* Imagine spelling

* Loicense

* perish

* Fix weightless status

Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2022-07-25 15:10:23 +10:00
committed by GitHub
parent aad6a22a6a
commit bfac53e7bc
36 changed files with 607 additions and 412 deletions

View File

@@ -1,7 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Client.Graphics;
namespace Content.Client.Parallax.Data

View File

@@ -1,12 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using JetBrains.Annotations;
using Content.Client.Resources;
using Content.Client.IoC;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Content.Client.Parallax.Data;

View File

@@ -62,5 +62,12 @@ public sealed class ParallaxLayerConfig
/// </summary>
[DataField("slowness")]
public float Slowness { get; set; } = 0.5f;
/// <summary>
/// Should the parallax scroll at a specific rate per second.
/// </summary>
[DataField("scrolling")] public Vector2 Scrolling = Vector2.Zero;
[DataField("shader")] public string? Shader = "unshaded";
}

View File

@@ -1,35 +1,30 @@
using System;
using Robust.Client.Graphics;
using Content.Client.Parallax;
using System.Threading.Tasks;
using Robust.Shared.Maths;
namespace Content.Client.Parallax.Managers;
public interface IParallaxManager
{
/// <summary>
/// The current parallax.
/// Changing this causes a new parallax to be loaded (eventually).
/// Do not alter until prototype manager is available.
/// Useful "csi" input for testing new parallaxes:
/// using Content.Client.Parallax.Managers; IoCManager.Resolve<IParallaxManager>().ParallaxName = "test";
/// </summary>
string ParallaxName { get; set; }
/// <summary>
/// All WorldHomePosition values are offset by this.
/// </summary>
Vector2 ParallaxAnchor { get; set; }
/// <summary>
/// The layers of the currently loaded parallax.
/// This will change on a whim without notification.
/// </summary>
ParallaxLayerPrepared[] ParallaxLayers { get; }
bool IsLoaded(string name);
/// <summary>
/// Used to initialize the manager.
/// The layers of the selected parallax.
/// </summary>
ParallaxLayerPrepared[] GetParallaxLayers(string name);
/// <summary>
/// Loads in the default parallax to use.
/// Do not call until prototype manager is available.
/// </summary>
void LoadParallax();
void LoadDefaultParallax();
Task LoadParallaxByName(string name);
void UnloadParallax(string name);
}

View File

@@ -1,111 +1,103 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Content.Client.Parallax.Data;
using Content.Shared;
using Content.Shared.CCVar;
using Nett;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Prototypes;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Content.Client.Parallax.Managers;
internal sealed class ParallaxManager : IParallaxManager
public sealed class ParallaxManager : IParallaxManager
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
private string _parallaxName = "";
public string ParallaxName
{
get => _parallaxName;
set
{
LoadParallaxByName(value);
}
}
private ISawmill _sawmill = Logger.GetSawmill("parallax");
public Vector2 ParallaxAnchor { get; set; }
private CancellationTokenSource? _presentParallaxLoadCancel;
private readonly ConcurrentDictionary<string, ParallaxLayerPrepared[]> _parallaxesLQ = new();
private readonly ConcurrentDictionary<string, ParallaxLayerPrepared[]> _parallaxesHQ = new();
private ParallaxLayerPrepared[] _parallaxLayersHQ = {};
private ParallaxLayerPrepared[] _parallaxLayersLQ = {};
private readonly ConcurrentDictionary<string, CancellationTokenSource> _loadingParallaxes = new();
public ParallaxLayerPrepared[] ParallaxLayers => _configurationManager.GetCVar(CCVars.ParallaxLowQuality) ? _parallaxLayersLQ : _parallaxLayersHQ;
public bool IsLoaded(string name) => _parallaxesLQ.ContainsKey(name);
public async void LoadParallax()
public ParallaxLayerPrepared[] GetParallaxLayers(string name)
{
await LoadParallaxByName("default");
if (_configurationManager.GetCVar(CCVars.ParallaxLowQuality))
{
return !_parallaxesLQ.TryGetValue(name, out var lq) ? Array.Empty<ParallaxLayerPrepared>() : lq;
}
return !_parallaxesLQ.TryGetValue(name, out var hq) ? Array.Empty<ParallaxLayerPrepared>() : hq;
}
private async Task LoadParallaxByName(string name)
public void UnloadParallax(string name)
{
// Update _parallaxName
if (_parallaxName == name)
if (_loadingParallaxes.TryGetValue(name, out var loading))
{
loading.Cancel();
_loadingParallaxes.Remove(name, out _);
return;
}
_parallaxName = name;
if (!_parallaxesLQ.ContainsKey(name)) return;
_parallaxesLQ.Remove(name, out _);
_parallaxesHQ.Remove(name, out _);
}
public async void LoadDefaultParallax()
{
await LoadParallaxByName("Default");
}
public async Task LoadParallaxByName(string name)
{
if (_parallaxesLQ.ContainsKey(name) || _loadingParallaxes.ContainsKey(name)) return;
// Cancel any existing load and setup the new cancellation token
_presentParallaxLoadCancel?.Cancel();
_presentParallaxLoadCancel = new CancellationTokenSource();
var cancel = _presentParallaxLoadCancel.Token;
// Empty parallax name = no layers (this is so that the initial "" parallax name is consistent)
if (_parallaxName == "")
{
_parallaxLayersHQ = _parallaxLayersLQ = new ParallaxLayerPrepared[] {};
return;
}
var token = new CancellationTokenSource();
_loadingParallaxes[name] = token;
var cancel = token.Token;
// Begin (for real)
Logger.InfoS("parallax", $"Loading parallax {name}");
_sawmill.Info($"Loading parallax {name}");
try
{
var parallaxPrototype = _prototypeManager.Index<ParallaxPrototype>(name);
ParallaxLayerPrepared[] hq;
ParallaxLayerPrepared[] lq;
ParallaxLayerPrepared[][] layers;
if (parallaxPrototype.LayersLQUseHQ)
{
lq = hq = await LoadParallaxLayers(parallaxPrototype.Layers, cancel);
layers = new ParallaxLayerPrepared[2][];
layers[0] = layers[1] = await LoadParallaxLayers(parallaxPrototype.Layers, cancel);
}
else
{
var results = await Task.WhenAll(
layers = await Task.WhenAll(
LoadParallaxLayers(parallaxPrototype.Layers, cancel),
LoadParallaxLayers(parallaxPrototype.LayersLQ, cancel)
);
hq = results[0];
lq = results[1];
}
// Still keeping this check just in case.
if (_parallaxName == name)
{
_parallaxLayersHQ = hq;
_parallaxLayersLQ = lq;
Logger.InfoS("parallax", $"Loaded parallax {name}");
}
_loadingParallaxes.Remove(name, out _);
if (token.Token.IsCancellationRequested) return;
_parallaxesLQ[name] = layers[0];
_parallaxesHQ[name] = layers[1];
_sawmill.Info($"Loaded parallax {name}");
}
catch (Exception ex)
{
Logger.ErrorS("parallax", $"Failed to loaded parallax {name}: {ex}");
_sawmill.Error($"Failed to loaded parallax {name}: {ex}");
}
}

View File

@@ -1,10 +1,7 @@
using Content.Client.Parallax.Managers;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Random;
using Robust.Shared.ViewVariables;
namespace Content.Client.Parallax;
@@ -28,7 +25,7 @@ public sealed class ParallaxControl : Control
protected override void Draw(DrawingHandleScreen handle)
{
foreach (var layer in _parallaxManager.ParallaxLayers)
foreach (var layer in _parallaxManager.GetParallaxLayers("Default"))
{
var tex = layer.Texture;
var texSize = tex.Size * layer.Config.Scale.Floored();

View File

@@ -1,47 +1,55 @@
using System;
using Content.Client.Parallax.Managers;
using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Parallax;
public sealed class ParallaxOverlay : Overlay
{
[Dependency] private readonly IParallaxManager _parallaxManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IParallaxManager _manager = default!;
private readonly ParallaxSystem _parallax;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowWorld;
private readonly ShaderInstance _shader;
public ParallaxOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
_parallax = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ParallaxSystem>();
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.Viewport.Eye == null)
{
if (args.MapId == MapId.Nullspace)
return;
}
if (!_configurationManager.GetCVar(CCVars.ParallaxEnabled))
{
return;
}
var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero;
var screenHandle = args.WorldHandle;
screenHandle.UseShader(_shader);
foreach (var layer in _parallaxManager.ParallaxLayers)
var layers = _parallax.GetParallaxLayers(args.MapId);
var realTime = (float) _timing.RealTime.TotalSeconds;
foreach (var layer in layers)
{
ShaderInstance? shader;
if (!string.IsNullOrEmpty(layer.Config.Shader))
shader = _prototypeManager.Index<ShaderPrototype>(layer.Config.Shader).Instance();
else
shader = null;
screenHandle.UseShader(shader);
var tex = layer.Texture;
// Size of the texture in world units.
@@ -52,10 +60,11 @@ public sealed class ParallaxOverlay : Overlay
// The effects of this are such that a slowness of 1 anchors the layer to the centre of the screen, while a slowness of 0 anchors the layer to the world.
// (For values 0.0 to 1.0 this is in effect a lerp, but it's deliberately unclamped.)
// The ParallaxAnchor adapts the parallax for station positioning and possibly map-specific tweaks.
var home = layer.Config.WorldHomePosition + _parallaxManager.ParallaxAnchor;
var home = layer.Config.WorldHomePosition + _manager.ParallaxAnchor;
var scrolled = layer.Config.Scrolling * realTime;
// Origin - start with the parallax shift itself.
var originBL = (args.Viewport.Eye.Position.Position - home) * layer.Config.Slowness;
var originBL = (position - home) * layer.Config.Slowness + scrolled;
// Place at the home.
originBL += home;
@@ -90,6 +99,8 @@ public sealed class ParallaxOverlay : Overlay
screenHandle.DrawTextureRect(tex, Box2.FromDimensions(originBL, size));
}
}
screenHandle.UseShader(null);
}
}

View File

@@ -0,0 +1,71 @@
using Content.Client.Parallax.Managers;
using Content.Shared.Parallax;
using Robust.Client.Graphics;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Parallax;
public sealed class ParallaxSystem : SharedParallaxSystem
{
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IParallaxManager _parallax = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private const string Fallback = "Default";
public override void Initialize()
{
base.Initialize();
_overlay.AddOverlay(new ParallaxOverlay());
SubscribeLocalEvent<ParallaxComponent, ComponentHandleState>(OnParallaxHandleState);
_protoManager.PrototypesReloaded += OnReload;
}
private void OnReload(PrototypesReloadedEventArgs obj)
{
_parallax.UnloadParallax(Fallback);
_parallax.LoadDefaultParallax();
foreach (var comp in EntityQuery<ParallaxComponent>(true))
{
_parallax.UnloadParallax(comp.Parallax);
_parallax.LoadParallaxByName(comp.Parallax);
}
}
public override void Shutdown()
{
base.Shutdown();
_overlay.RemoveOverlay<ParallaxOverlay>();
_protoManager.PrototypesReloaded -= OnReload;
}
private void OnParallaxHandleState(EntityUid uid, ParallaxComponent component, ref ComponentHandleState args)
{
if (args.Current is not ParallaxComponentState state) return;
component.Parallax = state.Parallax;
if (!_parallax.IsLoaded(component.Parallax))
{
_parallax.LoadParallaxByName(component.Parallax);
}
}
public ParallaxLayerPrepared[] GetParallaxLayers(MapId mapId)
{
return _parallax.GetParallaxLayers(GetParallax(_map.GetMapEntityId(mapId)));
}
public string GetParallax(MapId mapId)
{
return GetParallax(_map.GetMapEntityId(mapId));
}
public string GetParallax(EntityUid mapUid)
{
return TryComp<ParallaxComponent>(mapUid, out var parallax) ? parallax.Parallax : Fallback;
}
}