Skip to content

Commit

Permalink
Create game-specific Scene2.5D for drawing pips
Browse files Browse the repository at this point in the history
  • Loading branch information
charliefoxtwo committed Feb 4, 2024
1 parent 9b93648 commit 0c7e3df
Show file tree
Hide file tree
Showing 10 changed files with 498 additions and 435 deletions.
4 changes: 2 additions & 2 deletions src/OpenSage.Game/Graphics/Cameras/Camera.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed class Camera
private float _fieldOfView = MathUtility.ToRadians(50);

/// <summary>
/// Gets or sets a value that represents the camera's field of view in radians.
/// Gets or sets a value that represents the camera's field of view in radians.
/// </summary>
public float FieldOfView
{
Expand Down Expand Up @@ -155,7 +155,7 @@ public Vector3 WorldToScreenPoint(Vector3 position)
return _viewport.Project(position, Projection, View, Matrix4x4.Identity);
}

internal RectangleF? WorldToScreenRectangle(in Vector3 position, in SizeF screenSize)
public RectangleF? WorldToScreenRectangle(in Vector3 position, in SizeF screenSize)
{
var screenPosition = WorldToScreenPoint(position);

Expand Down
2 changes: 2 additions & 0 deletions src/OpenSage.Game/IGameDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public interface IGameDefinition

OnDemandAssetLoadStrategy CreateAssetLoadStrategy();

Scene25D CreateScene25D(Scene3D scene3D, AssetStore assetStore);

bool Probe(string directory) => File.Exists(Path.Combine(directory, LauncherExecutable));
}
}
296 changes: 296 additions & 0 deletions src/OpenSage.Game/Scene25D.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using OpenSage.Content;
using OpenSage.Graphics.Cameras;
using OpenSage.Gui;
using OpenSage.Logic;
using OpenSage.Logic.Object;
using OpenSage.Mathematics;

namespace OpenSage;

public class Scene25D(Scene3D scene3D, AssetStore assetStore)
{
protected GameObjectCollection GameObjects => scene3D.GameObjects;
protected Camera Camera => scene3D.Camera;
protected GameData GameData => assetStore.GameData.Current;

private Player LocalPlayer => scene3D.LocalPlayer;

public void Draw(DrawingContext2D drawingContext)
{
// The AssetViewer has no LocalPlayer
if (LocalPlayer != null)
{
HashSet<uint> propagandizedUnits = [];

foreach (var obj in GameObjects.Items)
{
if (obj.FindBehavior<PropagandaTowerBehavior>() is { } behavior)
{
foreach (var unitId in behavior.ObjectsInRange)
{
propagandizedUnits.Add(unitId);
}
}
}

foreach (var obj in GameObjects.Items)
{
if (obj.Hidden)
{
continue;
}

var focused = obj.IsSelected || LocalPlayer.HoveredUnit == obj;

if (obj.IsSelected || LocalPlayer.HoveredUnit == obj)
{
DrawHealthBox(drawingContext, obj);
}

DrawPips(drawingContext, obj, focused);

// todo: break out animations?
if (propagandizedUnits.Contains(obj.ID))
{
// todo: not sure how this visually stacks with other items
// todo: subliminal vs enthusiastic
AddAnimationToDrawable(obj, AnimationType.Enthusiastic);
}
else
{
RemoveAnimationFromDrawable(obj, AnimationType.Enthusiastic);
}

var healType = obj.IsKindOf(ObjectKinds.Structure) ? AnimationType.StructureHeal :
obj.IsKindOf(ObjectKinds.Vehicle) ? AnimationType.VehicleHeal : AnimationType.DefaultHeal;
if (!obj.IsKindOf(ObjectKinds.NoHealIcon) && obj.HealedByObjectId > 0)
{
// todo: how to tell if a unit is being healed by itself?
// a unit can be healing itself (usually from an upgrade like junk repair or veterancy, where healedbyobjectid is 0),
// or can be healed by another object (ambulance, hospital, prop tower, etc)
// animation DefaultHeal
AddAnimationToDrawable(obj, healType);
}
else
{
RemoveAnimationFromDrawable(obj, healType);
}

// todo: animations
// MoneyPickUp,
// LevelGainedAnimation, not an object animation?
// GetHealedAnimation, not an object animation?
// BombTimed,
// BombRemote,
// CarBomb,
// Disabled,
// AmmoFull, UNUSED?
// AmmoEmpty, UNUSED?

DrawAnimations(drawingContext, obj, scene3D.GameContext.GameLogic.CurrentFrame.Value);
}
}
}

protected virtual void DrawPips(DrawingContext2D drawingContext, GameObject gameObject, bool focused) { }

protected static BoundingSphere GetBoundingSphere(GameObject gameObject)
{
var geometrySize = gameObject.Definition.Geometry.Shapes[0].MajorRadius;

// Not sure if this is what IsSmall is actually for.
if (gameObject.Definition.Geometry.IsSmall)
{
geometrySize = Math.Max(geometrySize, 15);
}

return new BoundingSphere(gameObject.Translation, geometrySize);
}

private void DrawHealthBox(DrawingContext2D drawingContext, GameObject gameObject)
{
if (gameObject.Definition.KindOf.Get(ObjectKinds.Horde))
{
return;
}

var boundingSphere = GetBoundingSphere(gameObject);

var healthBoxSize = Camera.GetScreenSize(boundingSphere);

// todo: there should be some additional height being added here, but it's unclear what the logic should be
var healthBoxWorldSpacePos = gameObject.Translation.WithZ(gameObject.Translation.Z + gameObject.Definition.Geometry.Shapes[0].Height);
var healthBoxRect = Camera.WorldToScreenRectangle(
healthBoxWorldSpacePos,
new SizeF(healthBoxSize, 3));

if (healthBoxRect == null)
{
return;
}

void DrawBar(in RectangleF rect, in ColorRgbaF color, float value)
{
var actualRect = rect.WithWidth(rect.Width * value);
drawingContext.FillRectangle(actualRect, color);

var borderColor = color.WithRGB(color.R / 2.0f, color.G / 2.0f, color.B / 2.0f);
drawingContext.DrawRectangle(rect, borderColor, 1);
}

// TODO: Not sure what to draw for InactiveBody?
if (gameObject.HasActiveBody())
{
var red = 0f;
float green;
var blue = 0f;

if (gameObject.IsBeingConstructed())
{
green = (float)gameObject.HealthPercentage;
blue = 1;
}
else
{
red = Math.Clamp((1 - (float)gameObject.HealthPercentage) * 2, 0, 1);
green = Math.Clamp((float)gameObject.HealthPercentage * 2, 0, 1);
}

DrawBar(
healthBoxRect.Value,
new ColorRgbaF(red, green, blue, 1),
(float)gameObject.HealthPercentage);
}

var yOffset = 0;
if (gameObject.ProductionUpdate != null && !gameObject.IsBeingConstructed())
{
yOffset += 4;
var productionBoxRect = healthBoxRect.Value.WithY(healthBoxRect.Value.Y + yOffset);
var productionBoxValue = gameObject.ProductionUpdate.IsProducing
? gameObject.ProductionUpdate.ProductionQueue[0].Progress
: 0;

DrawBar(
productionBoxRect,
new ColorRgba(172, 255, 254, 255).ToColorRgbaF(),
productionBoxValue);
}

var gainsExperience = gameObject.FindBehavior<ExperienceUpdate>().ObjectGainsExperience;
if (gainsExperience)
{
yOffset += 4;
var experienceBoxRect = healthBoxRect.Value.WithY(healthBoxRect.Value.Y + yOffset);
DrawBar(
experienceBoxRect,
new ColorRgba(255, 255, 0, 255).ToColorRgbaF(),
gameObject.ExperienceValue / (float)gameObject.ExperienceRequiredForNextLevel);
}
}

private void DrawBottomLeftImage(DrawingContext2D drawingContext, GameObject gameObject, MappedImage image)
{
var boundingSphere = GetBoundingSphere(gameObject);

var xOffset = Camera.GetScreenSize(boundingSphere) / -2; // these just start where the health bar starts

var rankWorldSpacePos = gameObject.Translation with
{
Z = gameObject.Translation.Z + gameObject.Definition.Geometry.Shapes[0].Height - image.Coords.Height / 8f,
};

var size = image.Coords.Size.ToSizeF();
var propRect = Camera.WorldToScreenRectangle(
rankWorldSpacePos,
new SizeF(size.Width / 2, size.Height / 2)); // for some reason this seems to be half size

if (propRect.HasValue)
{
var rect = propRect.Value;
xOffset += rect.Width / 2;
drawingContext.DrawMappedImage(image, rect.WithX(rect.X + xOffset));
}
}

private void DrawTopCenteredImage(DrawingContext2D drawingContext, GameObject gameObject, MappedImage image)
{
var rankWorldSpacePos = gameObject.Translation with
{
Z = gameObject.Translation.Z + gameObject.Definition.Geometry.Shapes[0].Height + image.Coords.Height / 2f,
};

var propRect = Camera.WorldToScreenRectangle(
rankWorldSpacePos,
image.Coords.Size.ToSizeF());

if (propRect.HasValue)
{
var rect = propRect.Value;
drawingContext.DrawMappedImage(image, rect);
}
}

private static void AddAnimationToDrawable(GameObject gameObject, AnimationType animationType)
{
gameObject.Drawable.AddAnimation(animationType);
}

private static void RemoveAnimationFromDrawable(GameObject gameObject, AnimationType animationType)
{
gameObject.Drawable.RemoveAnimation(animationType);
}

private readonly List<AnimationType> _animationsToRemove = []; // instantiating this here instead of in-scope prevents allocations

private void DrawAnimations(DrawingContext2D context, GameObject gameObject, uint currentFrame)
{
_animationsToRemove.Clear();
foreach (var animation in gameObject.Drawable.Animations)
{
if (animation.SetFrame(currentFrame))
{
var image = animation.Current;

switch (animation.AnimationType)
{
case AnimationType.DefaultHeal:
case AnimationType.StructureHeal:
case AnimationType.VehicleHeal:
case AnimationType.Disabled:
case AnimationType.CarBomb:
DrawTopCenteredImage(context, gameObject, image);
break;
case AnimationType.Enthusiastic:
case AnimationType.Subliminal:
DrawBottomLeftImage(context, gameObject, image);
break;
case AnimationType.BombTimed:
case AnimationType.BombRemote:
// todo: the above animations appear centered above the health bar for vehicles, and offset to the side for structures
case AnimationType.MoneyPickUp: // unknown how this animation works yet
throw new NotImplementedException("animation not yet implemented");
case AnimationType.AmmoFull:
case AnimationType.AmmoEmpty:
throw new NotSupportedException("animation not supported by game engine");
case AnimationType.LevelGainedAnimation:
case AnimationType.GetHealedAnimation:
throw new NotSupportedException("animation not object-based"); // potentially remove these from AnimationType in the future?
default:
throw new ArgumentOutOfRangeException();
}
}
else
{
_animationsToRemove.Add(animation.AnimationType);
}
}

foreach (var animation in _animationsToRemove)
{
RemoveAnimationFromDrawable(gameObject, animation);
}
}
}
Loading

0 comments on commit 0c7e3df

Please sign in to comment.