Target a couple of hot paths: - In Actor, cache the renderables enumerable by tracking the last used WorldRenderer. It's slightly uncouth but in practise the WorldRenderer will be the same for the lifetime of the Actor and outlive it anyway. - In RenderSprites, cache the renderables enumerable. If we refresh the palettes directly, we don't need the WorldRenderer for anything else so can return a cached enumerable. This could result in iterating the animation list twice, so use a flag to only do it when required, which is rare. - In AutoTarget, the compiler has an unfortunate behaviour where it allocates the helper class for a capturing lambda at the top of the loop, but it's on a rare path and so this allocation is usually unused. We can discourage the compiler from allocating until it's actually needed by wrapping the call in a local function.
303 lines
8.5 KiB
C#
303 lines
8.5 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Frozen;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Mods.Common.Graphics;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits.Render
|
|
{
|
|
public interface IRenderActorPreviewSpritesInfo : ITraitInfoInterface
|
|
{
|
|
IEnumerable<IActorPreview> RenderPreviewSprites(ActorPreviewInitializer init, string image, int facings, PaletteReference p);
|
|
}
|
|
|
|
[Desc("Render trait fundament that won't work without additional With* render traits.")]
|
|
public class RenderSpritesInfo : TraitInfo, IRenderActorPreviewInfo
|
|
{
|
|
[Desc("The sequence name that defines the actor sprites. Defaults to the actor name.")]
|
|
public readonly string Image = null;
|
|
|
|
[Desc("A dictionary of faction-specific image overrides.")]
|
|
public readonly FrozenDictionary<string, string> FactionImages = null;
|
|
|
|
[PaletteReference]
|
|
[Desc("Custom palette name")]
|
|
public readonly string Palette = null;
|
|
|
|
[PaletteReference(true)]
|
|
[Desc("Custom PlayerColorPalette: BaseName")]
|
|
public readonly string PlayerPalette = "player";
|
|
|
|
public override object Create(ActorInitializer init) { return new RenderSprites(init, this); }
|
|
|
|
public IEnumerable<IActorPreview> RenderPreview(ActorPreviewInitializer init)
|
|
{
|
|
var sequences = init.World.Map.Sequences;
|
|
var faction = init.GetValue<FactionInit, string>(this);
|
|
var ownerName = init.Get<OwnerInit>().InternalName;
|
|
var image = GetImage(init.Actor, faction);
|
|
var palette = init.WorldRenderer.Palette(Palette ?? PlayerPalette + ownerName);
|
|
|
|
var facings = 0;
|
|
var body = init.Actor.TraitInfoOrDefault<BodyOrientationInfo>();
|
|
if (body != null)
|
|
{
|
|
facings = body.QuantizedFacings;
|
|
|
|
if (facings == -1)
|
|
{
|
|
var qbo = init.Actor.TraitInfoOrDefault<IQuantizeBodyOrientationInfo>();
|
|
facings = qbo?.QuantizedBodyFacings(init.Actor, sequences, faction) ?? 1;
|
|
}
|
|
}
|
|
|
|
foreach (var spi in init.Actor.TraitInfos<IRenderActorPreviewSpritesInfo>())
|
|
foreach (var preview in spi.RenderPreviewSprites(init, image, facings, palette))
|
|
yield return preview;
|
|
}
|
|
|
|
public string GetImage(ActorInfo actor, string faction)
|
|
{
|
|
if (FactionImages != null && !string.IsNullOrEmpty(faction) && FactionImages.TryGetValue(faction, out var factionImage))
|
|
return factionImage.ToLowerInvariant();
|
|
|
|
return (Image ?? actor.Name).ToLowerInvariant();
|
|
}
|
|
}
|
|
|
|
public class RenderSprites : IRender, ITick, INotifyOwnerChanged, INotifyEffectiveOwnerChanged, IActorPreviewInitModifier
|
|
{
|
|
static readonly (DamageState DamageState, string Prefix)[] DamagePrefixes =
|
|
[
|
|
(DamageState.Critical, "critical-"),
|
|
(DamageState.Heavy, "damaged-"),
|
|
(DamageState.Medium, "scratched-"),
|
|
(DamageState.Light, "scuffed-")
|
|
];
|
|
|
|
sealed class AnimationWrapper
|
|
{
|
|
public readonly AnimationWithOffset Animation;
|
|
public readonly string Palette;
|
|
public readonly bool IsPlayerPalette;
|
|
public PaletteReference PaletteReference { get; private set; }
|
|
|
|
bool cachedVisible;
|
|
WVec cachedOffset;
|
|
ISpriteSequence cachedSequence;
|
|
|
|
public AnimationWrapper(AnimationWithOffset animation, string palette, bool isPlayerPalette)
|
|
{
|
|
Animation = animation;
|
|
Palette = palette;
|
|
IsPlayerPalette = isPlayerPalette;
|
|
}
|
|
|
|
public void CachePalette(WorldRenderer wr, Player owner)
|
|
{
|
|
PaletteReference = wr.Palette(IsPlayerPalette ? Palette + owner.InternalName : Palette);
|
|
}
|
|
|
|
public void OwnerChanged()
|
|
{
|
|
// Update the palette reference next time we draw
|
|
if (IsPlayerPalette)
|
|
PaletteReference = null;
|
|
}
|
|
|
|
public bool IsVisible => Animation.DisableFunc == null || !Animation.DisableFunc();
|
|
|
|
public bool Tick()
|
|
{
|
|
// Tick the animation
|
|
Animation.Animation.Tick();
|
|
|
|
// Return to the caller whether the renderable position or size has changed
|
|
var visible = IsVisible;
|
|
var offset = Animation.OffsetFunc?.Invoke() ?? WVec.Zero;
|
|
var sequence = Animation.Animation.CurrentSequence;
|
|
|
|
var updated = visible != cachedVisible || offset != cachedOffset || sequence != cachedSequence;
|
|
cachedVisible = visible;
|
|
cachedOffset = offset;
|
|
cachedSequence = sequence;
|
|
|
|
return updated;
|
|
}
|
|
}
|
|
|
|
public readonly RenderSpritesInfo Info;
|
|
readonly string faction;
|
|
readonly List<AnimationWrapper> anims = [];
|
|
readonly IEnumerable<IRenderable> renderables;
|
|
bool shouldRefreshPalettes;
|
|
string cachedImage;
|
|
|
|
public static Func<WAngle> MakeFacingFunc(Actor self)
|
|
{
|
|
var facing = self.TraitOrDefault<IFacing>();
|
|
if (facing == null)
|
|
return () => WAngle.Zero;
|
|
|
|
return () => facing.Facing;
|
|
}
|
|
|
|
public RenderSprites(ActorInitializer init, RenderSpritesInfo info)
|
|
{
|
|
Info = info;
|
|
faction = init.GetValue<FactionInit, string>(init.Self.Owner.Faction.InternalName);
|
|
renderables = RenderAnimations(anims, init.Self);
|
|
}
|
|
|
|
public string GetImage(Actor self)
|
|
{
|
|
if (cachedImage != null)
|
|
return cachedImage;
|
|
|
|
return cachedImage = Info.GetImage(self.Info, faction);
|
|
}
|
|
|
|
public void UpdatePalette()
|
|
{
|
|
shouldRefreshPalettes = true;
|
|
foreach (var anim in anims)
|
|
anim.OwnerChanged();
|
|
}
|
|
|
|
public virtual void OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) { UpdatePalette(); }
|
|
public void OnEffectiveOwnerChanged(Actor self, Player oldEffectiveOwner, Player newEffectiveOwner) { UpdatePalette(); }
|
|
|
|
public virtual IEnumerable<IRenderable> Render(Actor self, WorldRenderer wr)
|
|
{
|
|
if (shouldRefreshPalettes)
|
|
{
|
|
shouldRefreshPalettes = false;
|
|
foreach (var a in anims)
|
|
{
|
|
if (a.PaletteReference == null)
|
|
{
|
|
var owner = self.EffectiveOwner != null && self.EffectiveOwner.Disguised ? self.EffectiveOwner.Owner : self.Owner;
|
|
a.CachePalette(wr, owner);
|
|
}
|
|
}
|
|
}
|
|
|
|
return renderables;
|
|
}
|
|
|
|
static IEnumerable<IRenderable> RenderAnimations(List<AnimationWrapper> anims, Actor self)
|
|
{
|
|
foreach (var a in anims)
|
|
{
|
|
if (!a.IsVisible)
|
|
continue;
|
|
|
|
foreach (var r in a.Animation.Render(self, a.PaletteReference))
|
|
yield return r;
|
|
}
|
|
}
|
|
|
|
public virtual IEnumerable<Rectangle> ScreenBounds(Actor self, WorldRenderer wr)
|
|
{
|
|
foreach (var a in anims)
|
|
if (a.IsVisible)
|
|
yield return a.Animation.ScreenBounds(self, wr);
|
|
}
|
|
|
|
void ITick.Tick(Actor self)
|
|
{
|
|
Tick(self);
|
|
}
|
|
|
|
protected virtual void Tick(Actor self)
|
|
{
|
|
var updated = false;
|
|
foreach (var a in anims)
|
|
updated |= a.Tick();
|
|
|
|
if (updated)
|
|
self.World.ScreenMap.AddOrUpdate(self);
|
|
}
|
|
|
|
public void Add(AnimationWithOffset anim, string palette = null, bool isPlayerPalette = false)
|
|
{
|
|
// Use defaults
|
|
if (palette == null)
|
|
{
|
|
palette = Info.Palette ?? Info.PlayerPalette;
|
|
isPlayerPalette = Info.Palette == null;
|
|
}
|
|
|
|
shouldRefreshPalettes = true;
|
|
anims.Add(new AnimationWrapper(anim, palette, isPlayerPalette));
|
|
}
|
|
|
|
public void Remove(AnimationWithOffset anim)
|
|
{
|
|
anims.RemoveAll(a => a.Animation == anim);
|
|
}
|
|
|
|
public static string UnnormalizeSequence(string sequence)
|
|
{
|
|
// Remove existing damage prefix
|
|
foreach (var s in DamagePrefixes)
|
|
{
|
|
if (sequence.StartsWith(s.Prefix, StringComparison.Ordinal))
|
|
{
|
|
sequence = sequence[s.Prefix.Length..];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return sequence;
|
|
}
|
|
|
|
public static string NormalizeSequence(Animation anim, DamageState state, string sequence)
|
|
{
|
|
// Remove any existing damage prefix
|
|
sequence = UnnormalizeSequence(sequence);
|
|
|
|
foreach (var s in DamagePrefixes)
|
|
if (state >= s.DamageState && anim.HasSequence(s.Prefix + sequence))
|
|
return s.Prefix + sequence;
|
|
|
|
return sequence;
|
|
}
|
|
|
|
// Required by WithSpriteBody and WithInfantryBody
|
|
public int2 AutoSelectionSize()
|
|
{
|
|
return AutoRenderSize();
|
|
}
|
|
|
|
// Required by WithSpriteBody and WithInfantryBody
|
|
public int2 AutoRenderSize()
|
|
{
|
|
return anims.Where(b => b.IsVisible
|
|
&& b.Animation.Animation.CurrentSequence != null)
|
|
.Select(a => (a.Animation.Animation.Image.Size.XY * a.Animation.Animation.CurrentSequence.Scale).ToInt2())
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits)
|
|
{
|
|
if (!inits.Contains<FactionInit>())
|
|
inits.Add(new FactionInit(faction));
|
|
}
|
|
}
|
|
}
|