2.x To 3.0 Upgrade Guide
This article is intended to be a useful resource for those that are familiar with GoRogue 2.x, and are looking to upgrade to 3.0, by summarizing/discussing the relevant changes. It is not an all-inclusive list of new features, however it discusses particularly breaking changes and some useful new features that result from them.
Upgraded to .NET Standard 2.1
One of the biggest changes in GoRogue 3 is that it has moved to support a minimum .NET Standard version of .NET Standard 2.1, for reasons pertaining to run-time performance and C# language feature support. This affects the minimum versions of various runtimes required to use GoRogue as described in this Microsoft-published table.
One of the biggest takeaways from this change is that support for .NET Framework has been removed entirely, as .NET Framework does not support .NET Standard 2.1 and there are no future plans for it to do so. Microsoft has officially pivoted away from adding new features to .NET Framework, and has committed to providing equivalent platform and feature support to their new .NET unified platform (previously .NET Core). GoRogue 3 does support .NET 5+ and .NET Core 3.0+.
For those of you that still have .NET Framework projects, you may be surprised at how easy it can be to upgrade; so this may not be a big issue. Some users, however, may have toolkit limitations that prevent an upgrade to a platform that supports .NET Standard 2.1 at the current time. Virtually all .NET runtimes, including Mono and the Unity compiler, have either already upgraded to support this version, or have stated that they have definitive plans to do so. Many game engines, including Unity, Godot, and Stride, have also either already upgraded or have stated that they will upgrade to a version of their runtime that will support it.
Support for C# 8 Nullable Reference Types
A benefit of the shift to .NET Standard 2.1 is that it has enabled GoRogue 3 to be annotated to support nullable reference types, which were introduced in C# 8. GoRogue's entire API now has annotations indicating proper nullability.
This will not break any code that chooses not to enable the nullable reference types feature; such code will simply receive no benefit. For projects that do enable it, however, it provides an extremely useful extra layer of compile-time safety.
Core Features Moved to TheSadRogue.Primitives
Another significant change is that a number of features which were in GoRogue 2 have been moved to a new library called TheSadRogue.Primitives. These types include:
Coord
(now calledPoint
)Direction
AdjacencyRule
,Distance
, andRadius
- The Lines class (with a slightly modified API)
RadiusAreaProvider
(moved toRadius
functions)- Everything in the
GoRogue.MapViews
namespace (renamed toGridViews
) MapArea
(now calledArea
) andIReadOnlyMapArea
(now calledIReadOnlyArea
)BoundedRectangle
Rectangle
- All spatial map related interfaces and implementations (moved to the
SadRogue.Primitives.SpatialMaps
namespace) IHasID
andIHasLayer
interfaces- Most of the
MathHelpers
static class
TheSadRogue.Primitives
is open source, and maintained by Thraka and I. It is compatible with all platforms that GoRogue is. Additionally, GoRogue depends on this library, so it will be automatically installed via NuGet when GoRogue is. Many changes will simply involve changing namespaces referred to from the GoRogue namespaces items were in, to SadRogue.Primitives
.
The primitives library also has some basic classes that weren't in GoRogue 2, such as Color
, Gradient
, Palette
, and PolarCoordinate
. These may be useful to replace equivalent types, and as well provide new distinct features.
The primitives library also has extension packages that allow its types to easily convert to the equivalent types in MonoGame, SFML, and some others; none of these extensions are required to use GoRogue, but they may be useful if you are using those libraries with GoRogue.
SadConsole, Thraka's console-emulation library, also uses TheSadRogue.Primitives
for its types as of v9, so those of you using GoRogue with SadConsole will enjoy benefits there as well.
Implicit Conversions to Equivalent MonoGame Types Removed
In GoRogue 2, types such as Coord
and Rectangle
had implicit conversions defined to their equivalent types in other libraries such as MonoGame, so that if you were using those libraries it was easy to convert between the two. These conversions were included in such a way as to not create a dependency on other libraries; unfortunately, however, some compilers (such as Unity's) did not parse the tags properly and considered these libraries required dependencies regardless. Thus, "MinimalDependency" versions were created and released for users that ran into these scenarios, which made getting started more complex.
In GoRogue 3, the implicit conversions have been removed. The types that had such conversions were moved to TheSadRogue.Primitives, and the conversions themselves have been replaced by the extension packages for that library. These extension packages define similar (explicit) conversion functions. See that library's documentation for details.
Value Tuples Removed from Public Interface
GoRogue 2 used value tuples introduced in C# 7 in some of its API; for example, the return type of the MapGeneration.Connectors.RoomDoorDonnector.ConnectRooms
function was a series of value tuples. Value types are very good for performance when used correctly, and value tuples make a convenient way of creating a basic value type. However, value tuples can make serialization more challenging, and are nowhere near as convenient to work with in other .NET languages. Therefore, GoRogue elects not to use value tuples in its public API, although it makes extensive use of value types.
Instead of returning or taking as input a value tuple, GoRogue algorithms take a class whose name typically ends in "Pair"; for example, ItemStepPair
or AreaConnectionPointPair
. These classes are implicitly convertible to and from a tuple of the two items, and implement support for deconstruction syntax. Therefore, they provide all the benefits of value tuples for C# users, and in fact can be used as if they are value tuples in most cases; but they also provide the benefits of serialization control and strongly-named values for non C# users.
Mutable Types No Longer Implement Equals/GetHashCode
In GoRogue 2, a number of mutable reference types implemented Equals
, operator==
, and GetHashCode
, such as MapArea
. However, according to Microsoft guidance, types that are mutable should not implement Equals
, due to semantics of the requirement to also implement GetHashCode
. Since Equals
, operator==
and GetHashCode
are supposed to be implemented in tandem for things to work correctly, such classes should ideally not implement any of those functions.
Therefore, Equals
, operator==
, and GetHashCode
implementations for any mutable types in GoRogue (including types in TheSadRogue.Primitives
library) have been removed. Instead, mutable types implement a new interface IMatchable
. It provides a function Matches
, whose signature is identical to IEquatable<T>.Equals
(except for the difference in names). This function can be used to perform the comparisons previously performed by the Equals
function implementations. Effectively, any calls on such types that in GoRogue 2 used the ==
operator or .Equals
for a custom comparison should now use .Matches
.
With the exception of custom iterators, all value types in GoRogue 3 are immutable, and immutable value types such as Point
and Rectangle
still implement Equals
, GetHashCode
, and IEquatable<T>
as they previously did. However, they also implement IMatchable
in a way equivalent to their Equals
function, for consistency.
Some Functions Return Custom Iterators Instead of IEnumerable
In GoRogue 2, a number of classes and structures had functions that returned IEnumerable<T>
in order to provide a list of items. Many functions still do this in GoRogue 3, as the concept works particularly well for creating a flexible API; the results work with LINQ and can easily be converted to List, array, and other data structures when needed. However, the use of IEnumerable<T>
can have negative effects on performance in some cases. This is primarily due to the fact that IEnumerable
is an interface, and therefore requires boxing and/or state to function, which creates work for the GC.
Therefore, in GoRogue 3, some such functions instead use a concept very similar to List<T>.Enumerator
to mitigate this. Instead of returning an IEnumerable
, they instead return a value type which is a custom iterator, whose name typically ends in Enumerator
; for example, RectanglePositionsEnumerator
. The return types of these functions implement IEnumerator<T>
and IEnumerable<T>
, so they can be used in a foreach loop and even can be used with LINQ if needed; however the performance, particularly when used directly in a foreach loop, is notably better because the compiler no longer has to box a value type into an interface reference. Rectangle.Positions()
is one such function, for example:
var rect = new Rectangle(1, 2, 3, 4);
// Positions() returns a RectanglePositionsEnumerator, but you can use it exactly as if it
// returned IEnumerable
foreach (var pos in rect.Positions())
Console.WriteLine(pos);
// You can even use it with LINQ
var positions = rect.Positions().ToArray();
Naming Conventions for Static Variables and Enums Changed
Names of various enumerations and static variables have been changed, to bring them more in accordance with recommended C# naming conventions. Effectively, this just results in ALL_CAPS style names being removed, and replaced with UpperFirstLetter style names; eg. Radius.SQUARE
has been changed to Radius.Square
.
MapView Changes (Now Called GridViews)
MapViews have undergone a refactor in GoRogue 3 as well.
Views Renamed and Moved
First, the term "map view" has been replaced by "grid view", and all of the classes have been renamed accordingly:
Old Name | New Name |
---|---|
IMapView<T> |
IGridView<T> |
ISettableMapView<T> |
ISettableGridView<T> |
ArrayMap<T> |
ArrayView<T> |
ArrayMap2D<T> |
ArrayView2D<T> |
LambdaMapView<T> |
LambdaGridView<T> |
LambdaSettableMapView<T> |
LambdaSettableGridView<T> |
LambdaTranslationMapView<T> |
LambdaTranslationGridView<T> |
LambdaSettableTranslationMapView<T> |
LambdaSettableTranslationGridView<T> |
TranslationMapView<T> |
TranslationGridView<T> |
SettableTranslationMapView<T> |
SettableTranslationGridView<T> |
Additionally, all of these classes have been moved to TheSadRogue.Primitives library, and reside under the namespace SadRogue.Primitives.GridViews
.
New GridView Base Classes
There is also a new base class that will make many custom IGridView
implementations simpler. In GoRogue 2, IMapView
implementations often contained repetitive code that implemented one or more indexers in terms of the others. To help alleviate this, TheSadRogue.Primitives
library that now contains grid views has the abstract classes GridViewBase
, SettableGridViewBase
, GridView1DIndexBase
, and SettableGridView1DIndexBase
.
These classes may optionally be inherited from as an alternative to manually implementing IGridView
and ISettableGridView
, respectively, and implement the repetitive code to define the location indexers in terms of a single, abstract indexer taking Point
. In the cases of the classes with "1DIndex" in the name, all indexers are implemented in terms of the one which takes a 1D index; the others implement all indexers in terms of the one which takes a Point
. For cases where there is nothing preventing your custom implementations from inheriting from a base class, this is much more convenient, as you need only implement the basic properties, and a single indexer; the other indexers are implemented based on that one.
New IGridView Properties
IGridView
and ISettableGridView
now require implementations of a Count
property, which should be equal to Width * Height
(the number of tiles in the view). This property allows IGridView
to support indices as introduced in C# 8. The base classes discussed above implement this property automatically.
New/Refactored Helper Functions
A number of useful extension methods for ISettableGridView
implementations have also been added/refactored. First, the ArrayMap.SetToDefault
function was changed to ArrayView.Clear()
to match more typical naming conventions for arrays. Additionally, an ApplyOverlay
overload has been added as an extension for ISettableGridView
that takes a Func<Point, T>
to use for determine overlay values, to avoid the need to use a LambdaGridView
in these cases. Finally, a Fill
extension method has been added that sets each location in an ISettableGridView
to a specified value.
New IGridView Implementations
Additionally, TheSadRogue.Primitives
contains a number of additional concrete implementation of ISettableGridView
that were not included in GoRogue 2. The first is DiffAwareGridView
. This grid view may be useful for situations where you are using a grid view or array of value types, and want to interact with or display incremental (groups of) changes to that grid view/array. See the class documentation for details.
The other is BitArrayView
, which is an ISettableGridView<bool>
implementation which is based on C#'s BitArray
. This should be used instead of ArrayView<bool>
in nearly all cases (unless your data is already an array and you're just creating a grid view wrapper), because its memory usage will be up to 8x less with a negligible difference in the performance of its operations.
Map Generation Rewritten
The map generation system has been completely rewritten from the ground up in GoRogue 3. It functions in a notably different manner, so it is recommended that you review this article which explains the new system's concepts and components.
GoRogue 2 Equivalents
The basic map generation algorithms that were present in GoRogue 2 are all present in some form in GoRogue 3, but not at a level that produces full implementation parity. Therefore, the new implementations of those algorithms are not guaranteed to produce the same level from a given seed that was produced in GoRogue 2.
Quick Generators
The "full-map generation" algorithms that were present in GoRogue 2 as functions in GoRogue.MapGeneration.QuickGenerators
are represented in GoRogue 3 as functions in GoRogue.MapGeneration.DefaultAlgorithms
. These functions produce a set of generation steps that, when executed in a Generator
, follow a roughly equivalent process:
Old QuickGenerator | DefaultAlgorithm |
---|---|
QuickGenerators.GenerateCellularAutomataMap |
DefaultAlgorithms.CellularAutomataGenerationSteps |
QuickGenerators.GenerateDungeonMazeMap |
DefaultAlgorithms.DungeonMazeMapSteps |
QuickGenerators.GenerateRandomRoomsMap |
DefaultAlgorithms.BasicRandomRoomsMapSteps |
QuickGenerators.GenerateRectangleMap |
DefaultAlgorithms.RectangleMapSteps |
Connection Algorithms
The "connection" algorithms provided in GoRogue 2 for connecting areas/rooms are provided as generation steps in GoRogue 3. The following shows a rough mapping:
Old Algorithm | New Step |
---|---|
MapGeneration.Connectors.ClosestMapAreaConnector |
MapGeneration.Steps.ClosestMapAreaConnection |
MapGeneration.Connectors.DeadEndTrimmer |
MapGeneration.Steps.TunnelDeadEndTrimming |
MapGeneration.Connectors.OrderedMapAreaConnector |
MapGeneration.Steps.OrderedMapAreaConnection |
MapGeneration.Connectors.RoomDoorConnector |
MapGeneration.Steps.RoomDoorConnection |
The configuration options are very similar functionally for most of them, however since these are now generation steps they're specified as public members of the class.
One notable exception is ClosestMapAreaConnector
; its GoRogue 3 counterpart functions somewhat differently. The largest change is that it utilizes the new MultiArea
class to more accurately represent the areas during the connection process, which allows it to take into account all points that are part of areas that have been previously joined by the algorithm. It also uses the ConnectionPointSelector
given to it not only to determine the points to use to connect the areas, but the points to use to determine distance between the areas as well, which allows for more control over its distance assumptions.
Tunnel Creation Algorithms
The tunnel creation algorithms provided in GoRogue 2 are largely intact in GoRogue 3, and have just moved to different namespaces. The API has changed a bit, but functionally they produce the same result. The ITunnelCreator
, DirectLineTunnelCreator
, and HorizontalVerticalTunnelCreator
classes are now located in the MapGeneration.TunnelCreators
namespace. The API has been modified so that the tunnel creators return an Area
representing the tunnel they create; but otherwise the algorithms function in much the same way. They are used as optional parameters in the appropriate map generation steps just like they were in the GoRogue 2 algorithms.
Connection Point Selection Algorithms
The algorithms for selecting area connection points, much like the tunnel creation algorithms, are largely intact but have moved to different namespaces. The API has some types that are changed, but generally the algorithms function the same way. The IAreaConnectionPointSelector
has been renamed to IConnectionPointSelector
, and it along with the CenterBoundsConnectionPointSelector
, ClosestConnectionPointSelector
, and RandomConnectionPointSelector
classes are now located in the MapGeneration.ConnectionPointSelectors
namespace. The API has been modified so that the SelectConnectionPoint
function returns a value type AreaConnectionPointPair
with the two selected points; that is implicitly convertible to a value tuple as detailed in this section.
Other Room/World Generation Items
Most other GoRogue 2 map generation structures/algorithms exist in some form as generation steps in the MapGeneration
or MapGeneration.Steps
namespaces. One notable exception is MapArea
, which was renamed to Area
and moved to TheSadRogue.Primitives
. The following is a rough mapping:
Old Algorithm | New Equivalent |
---|---|
MapGeneration.MapAreaFinder |
Unchanged * |
MapGeneration.MapArea |
TheSadRogue.Primitives.Area |
MapGeneration.BasicRoomsGenerator |
MapGeneration.Steps.RoomsGeneration ** |
MapGeneration.CellularAutomataAreaGenerator |
Multiple Steps *** |
MapGeneration.MazeGenerator |
MapGeneration.Steps.MazeGeneration |
MapGeneration.RoomsGenerator |
MapGeneration.Steps.RoomsGeneration |
* An actual map generation step that wraps this functionality is available in MapGeneration.Steps.AreaFinder
** Algorithm was similar to MapGeneration.RoomsGenerator
in GoRogue 2; the implementations are similar enough that they can reasonably be substituted for each other
*** MapGeneration.Steps.RandomViewFill
should be performed first to randomly fill walls/floors, then MapGeneration.Steps.CellularAutomataAreaGenerator
to smooth
New Generation-Related Algorithms
A number of new generation steps/algorithms that have no GoRogue 2 equivalent are also provided in the MapGeneration
namespace.
MultiArea
GoRogue now provides a MultiArea
class in the MapGeneration
namespace. This is a relatively simple class that implements the IReadOnlyArea
interface, by querying a list of "sub-areas". This class can prove useful because defining an area in terms of a set of other areas is a common need for map generation
and region representation.
PolygonArea
GoRogue also now provides a PolygonArea
class in the MapGeneration
namespace. This class also implements IReadOnlyArea
, and allows you to define the area based on a sequence of points/vertices. It keeps track of both the interior and exterior points of the polygon defined by those points, and allows rotation/mirroring of the area, and other useful operations.
Translation Steps
The article on map generation refers to "translation steps" as steps that purely transform existing data in the map generation context to a new form, as opposed to generating new, unique data. It may definitely prove useful to review these steps, located in the MapGeneration.Steps.Translation
namespace, as they may be useful in general cases.
Finding Doors
GoRogue now provides a generation step for finding where to place doorways, given wall-floor tile states and a series of rectangular rooms. The algorithm to do this is relatively simple, but it was requested a number of times as a feature in the past, and is much easier to implement generically in the new system. See DoorFinder class documentation for details.
New Structures
There are also a number of new data structures that may be useful for recording various forms of data during map generation. Most of these are in the MapGeneration.ContextComponents
namespace, or in the root MapGeneration
namespace. The API documentation explains the purpose of these classes in detail.
Component System Changes
The functionality of GoRogue's component system has been expanded in GoRogue 3, and has been moved to its own namespace. Additionally, some functions have changed names to make them more consistent with traditional collection names.
Classes and Function Names Refactored
All component-related classes have been moved to the GoRogue.Components
namespace. Additionally, classes have been renamed as follows:
Old Name | New Name |
---|---|
ComponentContainer |
Components.ComponentCollection |
IHasComponents |
Components.IComponentCollection |
ISortedComponent |
Components.ISortedComponent |
The names of functions on the component collection class/interface have also been modified to bring them more in-line with traditional C# collection names. A mapping of equivalent functions is as follows:
Old Name | New Name |
---|---|
AddComponent |
Add |
RemoveComponent |
Remove |
RemoveComponents |
Remove |
HasComponent |
Contains |
HasComponents |
Contains |
GetComponent |
GetFirstOrDefault |
GetComponents |
GetAll |
Note that GetFirstOrDefault
most closely replicates the behavior of GoRogue 2's GetComponent
; however, there is an additional function called GetFirst
that is now provided. This function throws an exception if a component of the specified type is not found, and may be useful to replace cases where you're checking if the return value of GetComponent
is null and erroring in that case.
Events Added to ComponentContainer
Another change is that in GoRogue 3, the Add
and Remove
functions on ComponentCollection
are no longer virtual. Instead, ComponentAdded
and ComponentRemoved
events have been added to ComponentCollection
, and these can be used to respond to the addition/removal of components.
Added Support for Tags on Components
Another change to the component system is that ComponentCollection
now supports associating a string "tag" with a component when it is added. When components are removed or retrieved, a tag can be specified in addition to the type parameter, and any component returned must have the tag specified as well as be of the type given. This can be useful if you need to manage multiple components that are of a single type or that share some inheritance or interface chain.
Additionally, the public API of ComponentCollection
is also contained within the IComponentCollection
interface. This is a very rough equivalent to the IHasComponents
interface in GoRogue 2. It is quite unlikely that a custom implementation of this interface would be necessary, but it is designed and used as such regardless.
ComponentCollection Focused on Composition
In GoRogue 2, the ComponentContainer
class presented an interface consistent for use via both inheritance, and composition. For example, you could have a ComponentContainer
class as a member of your class and use that field to store components attached to the object, or you could have your class inherit from ComponentContainer
directly.
Both are still possible in GoRogue 3, however because the interface for ComponentCollection
is now more similar to traditional C# collections (as discussed above), it now lends itself more easily to composition (like you would generally do with any C# collection). This falls in-line with changes to the IGameObject
system in GoRogue 3, and addresses many of the same ease-of-use issues. In general, this does not severely break APIs; any class that previously inherited from ComponentContainer
would simply have a property of type ComponentCollection
and store its components there.
Interface for Attaching Components
In order to conveniently support common use cases where you want to "attach" components to an object using this new approach, the IObjectWithComponents
interface has been added in the GoRogue.Components.ParentAware
namespace. This interface defines a single properly called GoRogueComponents
, of type IComponentCollection
. This accurately represents an object that has an associated ComponentCollection
which stores components attached to it.
The GoRogue.Components.ParentAware
namespace also contains an interface called IParentAwareComponent
. This interface specifies a single Parent
field, of type object?
. This is designed to capture the concept of a component which is aware of the parent object it's attached to. The ParentAwareComponentBase
class in the same namespace takes this farther by implementing this interface, and also providing events fired when it is attached to/detached from an object. It also provides useful functionality for using those events to enforce certain invariants upon components. ParentAwareComponentBase<T>
adds onto the non-generic class by automatically enforcing that the object it's attached to is of a particular type, and exposing the Parent
field as that type, which is useful to avoid constantly needing to cast the Parent
field to the type you need.
The above concepts conveniently represent interfaces involved in attaching components to an object; to complete the implementation, ComponentCollection
has built-in support for these interfaces. ComponentCollection
takes an optional parameter at construction of type object?
. If this parameter is specified, the value given is automatically set to the Parent
field of any IParentAwareComponent
that is added to it. Similarly, the Parent
field is set to null
when the component is removed. Components that do not implement this interface are still allowed, however ones that do implement it have their Parent
property automatically updated.
Putting these features together allows you to write components that are attached to an arbitrary object, and are aware of their "Parent" and capable of interacting with it. See GoRogue's GameFramework.GameObject
and GameFramework.Map
classes for examples of how to create such objects.
Factory System Changes
The factory system in GoRogue 3 has been refactored to address some naming/usability concerns. First, note that the GoRogue.Factory
namespace is now GoRogue.Factories
, which more accurately matches naming conventions elsewhere in the library.
Additionally, factory classes can now support blueprint IDs of arbitrary types, rather than only strings. This is implemented via an additional type parameter passed to the factory classes called TBlueprintID
.
The remaining factory changes fall in line with splitting out the class structures for the two main use cases of factories; to create items when additional configuration information per instance is needed, and to create items when such information is not needed. In GoRogue 2, both use cases were supported but the class structure for supporting this was somewhat unintuitive; it utilized an arbitrary class BlueprintConfig
for a base configuration object, that served no purpose other than to provide a default value. In GoRogue 3, the factory classes have been split out as follows:
Factory<TBlueprintID, TProduced>
- Produces objects of type
TProduced
viaIFactoryBlueprint
objects that DO NOT take a configuration object in theCreate
function. - Useful for cases that would previously have involved
SimpleBlueprint
- Produces objects of type
AdvancedFactory<TBlueprintID, TBlueprintConfig, TProduced>
- Produces objects of type
TProduced
viaIAdvancedFactoryBlueprint
objects that DO accept a configuration object of typeTBlueprintConfig
in theCreate
function. - Useful for cases that would have previously involved passing a configuration object of some type that subclassed
BlueprintConfig
- In GoRogue 3, there is NO required base class for configuration objects; it can be an arbitrary type
- Produces objects of type
IFactoryObject
will function the same way as it did, whether objects that implement it are created in a Factory
or AdvancedFactory
. As well, a new ItemNotDefinedException
with a useful message is thrown when their Create
or GetBlueprint
methods are called with a blueprint name that does not exist.
Additionally, some helper classes were added that make blueprints signifincantly easier to implement. These include
LambdaFactoryBlueprint<TBlueprintID, TProduced> and LambdaAdvancedFactoryBlueprint<TBlueprintID, TBlueprintConfig, TProduced>. These classes allow you to specify the Create
function as a Func
, which enables you to avoid implementing the blueprint interfaces via a subclass in many cases. Details on their intended usage can be found in the Factories how-to article.
Spatial Map Changes
A number of changes have been made to the interface for spatial maps in GoRogue 3. First, all spatial map implementations and related interfaces have been moved to the TheSadRogue.Primitives, under the namespace SadRogue.Primitives.SpatialMaps
. There are also a number of API changes.
API Refactor
In GoRogue 2, functions for spatial maps like Add
and Move
simply returned false
if an operation failed. This design had some useful properties, but ultimately turned out to be a poor design decision, which didn't fit well with the rest of the library and was the source of many known bugs/desyncs that users, and I, accidentally created in code. Therefore, the ISpatialMap
interface and all implementing classes have been modified such that, if their functions fail, they throw an ArgumentException
with an error message that tells you exactly what went wrong. Functions that work in this way now include GetPositionOf
(previously called GetPosition
), Add
, Move
, MoveAll
, and Remove
.
This change makes it much more obvious to the user when something unexpected happens. For situations where you do potentially expect the operation to fail, versions of the functions are provided that begin with Try
; eg. TryMove
, TryAdd
, TryRemove
TryGetPositionOf
, etc. These default to the old behavior returning true or false based on success. If you simply need to check whether an operation is possible, the interface also provides functions for this; these functions include CanAdd
, CanMove
, CanMoveAll
, etc.
Another associated change to spatial maps is the separation of the functionality for moving all items at a location vs. moving only the ones that can move. In addition to Move(T item)
, ISpatialMap
now also contains MoveAll
and MoveValid
functions. MoveAll
attempts to move all items at a given location to the target location, throwing an ArgumentException
if this fails. There is also a TryMoveAll
function, which returns false instead of throwing an exception. MoveValid
only moves the items that can move, returning precisely which items were moved.
Finally, note that, as implied above, GetPosition
has been renamed to GetPositionOf
. There are also a number of similar methods provided that handle the case where the position does not exist differently; these include GetPositionOfOrNull
and TryGetPositionOf
.
Position Syncing
In GoRogue, spatial maps store a position for each object stored within them. The Move
functions and similar modify this position. This is useful because it allows spatial maps to be self-contained; objects added to them don't have to have their own position field or meet any sort of arbitrary interface. However, it also adds complexity to their usage for some use cases, because it is common that map objects will store their own position inside of a field. When such objects are used in spatial maps, care must be taken to ensure that the spatial map's position for that object, and the position within the object's field, remain in sync.
In GoRogue 3, some new variants of spatial maps have been added to help make these use cases easier to manage. These include AutoSyncSpatialMap
, AutoSyncMultiSpatialMap
, and AutoSyncLayeredSpatialMap
. These variants require that the items in them implement the new IPositionable
interface, which contains a Position
property and some events that are fired when that property changes. It uses these events to automatically keep the Position
property, and the spatial map's internal record of the object's position, in sync. You may modify the Position
property directly, or call any of the spatial map's move functions; and in either case, both the field and the spatial map's record update automatically.
FOV Changes
In GoRogue 2, FOV functionality consisted of a single class, FOV
, which implemented a recursive shadowcasting FOV algorithm. GoRogue 2 also contained the IReadOnlyFOV
interface, which was useful for exposing the result of FOV without allowing modification.
This worked well initially because users could either use the built-in FOV class, or create their own arbitrary algorithm if desired. When GameFramework
was introduced, however, this became problematic because its Map
class had a property of type FOV
; so if a user wanted to use a custom FOV algorithm, they had to give up the explored-tile functionality and anything else pertaining to FOV that the map provided.
Introduction of Abstraction
GoRogue 3 addresses this by introducing a customizable abstraction for FOV calculations. First, the FOV
class has been renamed to RecursiveShadowcastingFOV
and moved to the GoRogue.FOV
namespace. This new namespace also contains IReadOnlyFOV
. It also has an additional interface IFOV
, which RecursiveShadowcastingFOV
now implements. This interface contains the entire public API of what used to be called FOV
, thus representing an abstraction over a method of calculating FOV. Finally, this namespace also contains FOVBase
, which is an abstract base class that implements IFOV
and simplifies the interface by ensuring that a minimal subset of functions must be implemented by a user.
In turn, Map
now has a property of type IFOV
for the player's FOV. This allows a user to implement a custom FOV calculation and use it within the map framework.
Change in Interface
In GoRogue 2, FOV classes implemented IMapView<double>
in order to allow you to access the results. In GoRogue 3, this is no longer the case; instead, there is a DoubleResultView
property that exposes the results of the FOV calculation as a grid view. Similarly, the BooleanFOV
property has been renamed to BooleanResultView
. This change more easily facilitated the abstraction that was introduced, and as well provided a more consistent interface.
GameFramework Namespace Refactored
A number of refactors have been performed on the GoRogue.GameFramework
namespace.
Game Object Changes
IGameObject
and the way it is used has undergone a notable refactor to address some usability issues present in GoRogue 2.
Simplification of IGameObject Implementation
The class structure for GameFramework.GameObject
has been simplified in GoRogue 3. In GoRogue 2, support for use cases where you were unable to inherit from GameObject
involved writing fairly complex code. You had to implement IGameObject
in these cases; but, since implementation of the functionality defined by IGameObject
was closely coupled with the code for GameFramework.Map
, this was non-trivial. The recommended approach for this in GoRogue 2 was to use a GameObject
instance as a private backing field for your IGameObject
implementation; and in fact GoRogue 2 had special support for this built into GameObject
in the form of the parent
parameter to the constructor.
However, this approach proved to be somewhat error prone, and very easily led to non-intuitive/difficult to debug behavior in error cases. Furthermore, it added complexity to code that needed to implement IGameObject
instead of inheriting from GameObject
. So, in GoRogue 3, the parent
parameter in the GameObject
constructor has been removed, and it is no longer recommended (and in many cases no longer possible) to implement IGameObject
via a backing field of type GameObject
.
Instead, a focus was placed on decoupling the code required to implement IGameObject
from the internal code in Map
, which ultimately resulted in IGameObject
being much easier to implement the more traditional way. Helper functions have been provided that make implementing most functions in the IGameObject
interface a one-line endeavor. As such, if you need to implement IGameObject
yourself, you should be able to more or less copy-paste the code from GameObject
, and use it for the implementation, without making your code unintuitive or unnecessarily long.
Removed IsStatic
In GoRogue 2, GameObject
instances had an IsStatic
flag, that could be set via a constructor parameter to indicate that they could not move. Objects on the terrain layer of a map (layer 0) were required to have IsStatic
set, to allow for some optimization by allowing them to reside on a grid view as opposed to a spatial map. This was useful for performance, but inconvenient at times for users; particularly if a user wanted to use something like GoRogue's factory system for creation of terrain. The position could only be set at construction due to the IsStatic
flag being set, which meant that the position had to be known exactly when the instance was created. Furthermore, the restriction of terrain objects being unable to move at all is actually more stringent than required; the only required portion for optimization is that they not move while they are part of a map.
Therefore, in GoRogue 3 the IsStatic
flag has been removed. Instead, if an object is on layer 0 of a map and it is moved, an exception will be thrown by the Map
. Objects on layer 0 that are not added to a Map
can be moved freely. This allows the optimization to stay, but makes creation of objects more convenient for a user.
Components Now Attached to a Property
In GoRogue 2, IGameObject
implemented IHasComponents
to allow components to be placed on game objects. In GoRogue 3, this is no longer the case; instead, IGameObject
defines a property GoRogueComponents
, which is a collection that can have components added/removed. This class also integrates the new "tag" functionality for tagging components. The names of the functions for adding, removing, and retrieving components have also changed in a manner corresponding to the component collection name changes.
The GoRogueComponents
field in GameObject
defaults to being of type Components.ComponentCollection
, and it is generally rare to need that field to be represented by some other type. Nonetheless, the field is of type Components.IComponentCollection
, and an instance of some custom implementation of this interface may be passed to GameObject
via its constructor. This ensures that the component collection structure can be customized if it becomes necessary.
Setting Position or Walkability may Throw Exception
In support of the spatial map changes in GoRogue 3, setting the Position
or IsWalkable
properties of a game object will throw an exception if they are set to a value that would violate map collision rules. See the corresponding Map changes section for details.
Optional Base Class for Components
GoRogue 2 contained the GameFramework.Components.IGameObjectComponent
interface, which was an optional interface that you could implement on components attached to a GameObject
. When implemented, the object would automatically have its Parent
property value updated to reflect the IGameObject
that it was attached to. In GoRogue 3, this interface has been removed, and replaced with the more generic system defined in the Components.ParentAware
namespace (as discussed here). This new system is simply a more generic version of IGameObjectComponent
, and provides nearly equivalent functionality.
This also means that IGameObject
components may utilize and benefit from the ParentAwareComponentBase
and ParentAwareComponentBase<T>
defined in the Components.ParentAware
namespace. This can be very convenient if you need to safely represent advanced concepts like having components be able to access methods/properties of their specific parent type.
ID Generation Changes
One additional change alters the method that you use to assign IDs to game objects in a custom way. In GoRogue 2, to assign IDs in a custom manner, you had to subclass GameObject
and override the GameObject.GenerateID()
function. Given that GameObject
instances can often otherwise be used with no subclasses at all, the requirement to create a subclass for this functionality proved inconvenient.
Therefore, in GoRogue 3, the GameObject.GenerateID()
function has been removed. Instead, an optional parameter of type Func<uint>
can be passed to the GameObject
constructor. If the parameter is passed, the given function is used to generate an ID. If not, the default method of randomly generating an ID is used. This still allows a custom method to be used, but does not require that a user create a subclass to do so.
Map Functionality Changes
There were also a number of relevant changes to the GameFramework.Map
API in GoRogue 3.
FOV Changes
In GoRogue 2, the Map
had CalculateFOV
functions that were used to calculate FOV for the map. This made it more difficult to separate FOV from the map in use cases that required it, and required users to implement inconvenient overrides of the CalculateFOV
functions in order to react to FOV changes. In GoRogue 3, a number of refactors have taken place to address this.
First, the Map.FOV
property has been renamed to Map.PlayerFOV
. This more clearly reflects the intended purpose of the field in games using multiple FOV instances for different entities. Similarly, the Explored
field has been renamed to PlayerExplored
.
Additionally, the CalculateFOV
functions have been completely removed from Map
. Instead, the PlayerFOV
field is of type IFOV
instead of IReadOnlyFOV
. This allows you to call the PlayerFOV.Calculate
function directly, with the same parameters previously passed to Map.CalculateFOV
. The FOV system has also been augmented to include a Recalculated
event that fires whenever the FOV is recalculated, which allows you to respond to the FOV changing. Since the PlayerFOV
field is still settable, these changes make it much easier to separate the FOV from the Map
when necessary and makes responding to FOV changes more convenient.
Components Allowed on Map
One other change is that GameFramework.Map
now has a GoRogueComponents
property. This allows you to attach components to a Map
just like you do with game objects. This may be useful functionality for component-based architectures. These components may also make use of the system defined in Components.ParentAware
.
RemoveTerrain Functionality Added
A RemoveTerrain
function has been added that takes a terrain object and removes it from the map, in support of ensuring that SetTerrain
can be annotated as taking non-null parameters. There is also a RemoveTerrainAt
function to allow you to remove whatever terrain is at a given position.
Naming Conventions for Position-Based Functions Changed
Functions that operate based on a position have changed name slightly; these functions will have "At" appended to the end of the name. For example, Map.GetTerrain
is now Map.GetTerrainAt
, Map.GetEntity
is now Map.GetEntityAt
, and so forth.
Adding/Removing/Moving Objects May Throw Exception
In GoRogue 3, spatial maps have been changed to throw exceptions in cases where invalid operations are performed (see the corresponding section). Because GameFramework.Map
is partially based upon spatial maps, and it exposes a number of similar operations via its API, similar changes have been made to Map
and IGameObject
.
The AddEntity
, RemoveEntity
, SetTerrain
, RemoveTerrain
and RemoveTerrainAt
functions all return void
and throw ArgumentException
if they are called with a value that doesn't meet the necessary criteria, with an exception message detailing exactly what the issue was. Similarly, Map
is configured in such a way that if an object's Position
or IsWalkable
properties are set to something that is invalid, then an InvalidOperationException
with a message detailing the issue will be thrown.
There are also extension methods provided for IGameObject
that return a boolean value indicating whether it is valid to set the object's Position
or an IsWalkable
property a certain way. The CanMove
function returns whether or not an object's position can be set; CanMoveIn
is similar, except it takes a direction and returns whether or not the object can move in that direction. Similarly, CanSetWalkability
and CanToggleWalkability
return information about whether or not IsWalkable
can be set.
RadiusAreaProvider Functionality Moved
The functionality of RadiusAreaProvider
was moved to TheSadRogue.Primitives
, and merged into the Radius
class in the form of Radius.PositionsInRadius
functions.
Random Number Generation Rewritten
In GoRogue 2, random number generation was handled by way of the Troscheutz.Random library. This worked reasonably, however there were a number of issues that required workarounds for some use cases of GoRogue:
- At least some tests for statistical quality find issues with many, if not most, of Troschuetz's random number generator implementations. Some are more robust and do not fail, but these are mostly the slower generators in the library.
- Many of the generators pass older, simpler tests like the first version of DIEHARD, but fail some tests in newer and more stringent suites like PractRand.
- Some generators are old enough that flaws are well-known, such as the Mersenne Twister.
- Others have been superseded by later generators, like XorShift128+ and its improved successors xoroshiro128** and xoshiro256**.
- Some also have dubious licensing restrictions (the three NR3 generators, only one of which passes more than a minute of quality testing)
- Troschuetz's API for generators does not expose the state of the generators, and the generators can only be serialized out of the box via select serialization methods (eg. serialization methods that support
[Serializable]
).- Therefore, when users needed to use a serialization method which was unsupported by Troschuetz directly, they had to simply record the seed value used to create the RNG, and then manually advance the generator state upon deserialization to get it back to what it was when the generator was serialized.
- This method could be performance intensive, and in some cases impractically so, and also tended to make testing and debugging more difficult.
- Troschuetz's API does not directly expose functionality to generate longs, ulongs, floats, or decimals.
ShaiRandom Overview
In an effort to provide better solutions to these issues, GoRogue 3 has moved away from Troschuetz, and instead uses a random number generation library called ShaiRandom. In addition to implementing a completely different set of generation algorithms, it provides a number of other benefits:
- Generator states are publicly exposed, and settable.
- This allows much more fine-tuned manipulation of generators, for both testing and serialization purposes.
- Generators all support a method of serializing to and from a string
- This remains independent of any particular serialiation method, but still provides easy ways to handle saving and restoring generator states
- Generators support "previous" and "skip" operations where possible
- Generators support long, ulong, float, and decimal generation
- ShaiRandom can utilize more modern C# features, since it only supports back to .NET Standard 2.1
- This allows some performance optimizations such as using spans for serialization/deserialization, as well as some convenience features
ShaiRandom also provides a number of other benefits; among them are some additional extension methods, wrappers and helpers that can assist in replicating and debugging issues with algorithms using RNG, and better run-time performance than both the Troschuetz generators and System.Random
. for details, see ShaiRandom's documentation.
Porting Guide
Despite the improvements, from a functional perspective, ShaiRandom is very similar to Troschuetz. The most subtle differences pertain to the minimum/maximum bounds of values returned from some similarly named functions and interfaces.
Therefore, although the differences between the two libraries are non-trivial, most of any difficulties porting existing code will likely be due to differences in the bounds of parameterless generation functions, if you are using any of them. The following sections will attempt to outline the changes most likely to be relevant when porting existing GoRogue 2 or Troschuetz-based code over to use GoRogue 3 and ShaiRandom. Note that there is a substantial amount of ShaiRandom functionality that is not mentioned here (because it has no direct Troschuetz equivalent); feel free to look through ShaiRandom's API documentation for details on those features.
Interface/Function Names
In Troschuetz, IGenerator
is the interface used to accept an arbitrary RNG. ShaiRandom
has a similar interface called IEnhancedRandom
. However, the names of equivalent functions between these two interfaces differ in some cases. The following table lists a rough mapping of Troschuetz function names to ShaiRandom ones:
Troschuetz Name | ShaiRandom Name |
---|---|
Next |
NextInt * |
NextUInt |
NextUInt * |
NextInclusiveMaxValue |
NextInt ** |
NextUIntInclusiveMaxValue |
NextUInt ** |
NextBoolean |
NextBool |
NextBytes |
NextBytes |
NextDouble |
NextDouble |
* The ShaiRandom functions treat bounds (and 0-parameter versions) differently than Troschuetz does; see the below sections for details.
** As described below, the 0-parameter versions of the listed ShaiRandom functions are inclusive on max value.
Zero-Parameter Integer Function Bounds
In Troschuetz, unless otherwise specified, integer-generation functions which take 0 parameters are exclusive on the type's max-value; eg. IGenerator.Next()
can return values in range [int.MinValue, int.MaxValue)
, IGenerator.NextUInt()
can return values in range [uint.MinValue, uint.MaxValue)
, and so on.
In ShaiRandom
, the integral type generation functions which take 0 parameters are inclusive on their max bound; eg. IGenerator.NextInt()
can return values in range [int.MinValue, int.MaxValue]
, IGenerator.NextUInt()
can return values in range [uint.MinValue, uint.MaxValue]
, and so on.
Note that this applies only to integer generation functions; the 0-parameter floating point functions in both ShaiRandom and Troschuetz both return a value in the range 0 (inclusive) to 1.0 (exclusive).
Bounded Generation Function Contracts
Both Troschuetz and ShaiRandom have various generator functions which take either one or two bounds, and guarantee that the number returned lies within those two bounds. However, how those bounds are interpreted differs between the two libraries.
The biggest difference for functions taking a single bound, is that Troschuetz interprets that bound as a "maximum" value, whereas ShaiRandom interprets the bound as simply an "outer" bound. In Troschuetz, for instance, even for Next(int)
or NextDouble(double)
functions, the bound specified must be greater than or equal to 0 (passing it anything else results in an exception). ShaiRandom does not have this limitation. The following table describes the behavior for ShaiRandom's generation functions which take a single bound:
Bound Relation | Allowed Range of Returned Value |
---|---|
bound > 0 | [0, bound) (eg. 0 (inclusive) to bound (exclusive)) |
bound == 0 | 0 |
bound < 0 | (bound, 0] (eg. bound (exclusive) to 0 (inclusive)) |
Similar logic applies for functions that take two bounds. Troschuetz asserts that the first bound is a "minimum" value, and the second a "maximum", whereas ShaiRandom interprets the first as an "inner" bound and the second as an "outer" bound. In Troschuetz, the second bound must always be greater than or equal to the first; the bounds are not allowed to cross each other. ShaiRandom allows this, and defines the behavior as follows:
Bound Relation | Allowed Range of Returned Value |
---|---|
inner < outer | [inner, outer) (eg. inner (inclusive) to outer (exclusive)) |
inner == outer | inner |
inner > outer | (outer, inner] (eg. outer (exclusive) to inner (inclusive)) |
Note that the inclusivity and exclusivity of the bounds as outlined above refers to the bounds for "typical" functions; eg. the bounds for NextInt
, NextUInt
, NextDouble
, etc; anything that does not have Inclusive
or Exclusive
specifically in the function name. ShaiRandom does provide a number of functions for generating floating point numbers that specify otherwise; for example, NextInclusiveDouble
follows the same logic but considers both bounds as inclusive, and NextExclusiveDouble
is similar but considers both bounds to be exclusive. These functions will clearly note how they interpret the bounds in the API documentation.
Seeding and Serialization
In Troschuetz, generators exposed a Seed
property, which you could use to query the seed used to initialize a random number generator. In ShaiRandom, the state is exposed directly, and can be accessed via either the IEnhancedRandom.SelectState
function, or various properties specific to generator implementations. Because of this, the API handles seeding from a single value differently than Troschuetz.
In ShaiRandom, you may call the IEnhancedRandom.Seed(ulong)
function on any generator implementation, in order to initialize it in a deterministic way based on the given seed value. However, once you call this function, there is no way to retrieve the value that you passed to that function; in most generator implementations, it won't be stored directly in the state fields or any property you can access. Instead, the Seed(ulong)
function implementation will take that single ulong
passed to it, and use it to initialize all of the state variables of the generator (deterministically, of course).
Functionally, nothing is lost here. In fact, ShaiRandom offers a lot of other APIs to interact with generator state directly; and so ShaiRandom is functionally more robust than Troschuetz in this context. Nevertheless, this may affect existing code in that it may change how it serializes data or initializes generators to a particular state.
There are two main ways you can go about replicating and controlling a generator's state:
- Utilize the serialization ShaiRandom provides. For any generator, the
StringSerialize()
function will produce a string of ASCII characters which encompasses the generators entire state. This string can be passed to theAbstractRandom.Deserialize()
function, which will create a new generator of the same type, with exactly the same state as the one that was serialized. - Initialize the generator context with the
Seed(ulong)
function, and record the seed used before you pass it, so that you may use it again in the future. Although not as flexible as other methods, it is relatively similar to the method of recording theSeed
property value on Troschuetz generators.
Furthermore, you can also control a generator's state directly via IEnhancedRandom's SetState
, SetSelectedState
, and SelectState
functions, as well as properties specific to each generator implementation. The PreviousULong
and Skip
methods may also be useful for controlling generator state. ShaiRandom's API documentation provides more specific information on these methods.
RNG Implementations Moved
In GoRogue 2, GoRogue had a number of random number "generator" implementations in the GoRogue.Random
namespace. These included KnownSeriesRandom
, which allowed a user to specify exactly what numbers to return from the generation functions (useful for unit testing), as well as MinRandom
and MaxRandom
, which effectively implemented the generation functions to return the minimum and maximum possible values, respectively, given the specified bounds.
In GoRogue 3, these implementations have all been moved to ShaiRandom, and are located in the ShaiRandom.Generators
namespace. Furthermore, their functionality has been expanded upon significantly; all of these generators support the full suite of IEnhancedRandom
number generation functions, and all of them are serializable using ShaiRandom's string-based serialization method.
ArchivalWrapper
An ArchivalWrapper
generator exists in the ShaiRandom.Wrappers
namespace, which drastically augments the usefulness of KnownSeriesRandom
. ArchivalWrapper
wraps another IEnhancedRandom
implementation and allows you to very easily reproduce the values produced by the generator it's wrapping. Once created, the ArchivalWrapper
can be passed to any arbitrary algorithm taking an IEnhancedRandom
implementation. Then, after the algorithm uses it, the wrapper can create a KnownSeriesRandom
that will produce exactly the same sequence of numbers produced by the generator it wrapped.
This can be extremely useful for debugging and unit testing, because it requires you to have no knowledge of the inner workings of an algorithm to reliably reproduce an issue with it; and the ability to reproduce that issue is completely independent of any RNG implementation, and anything else that RNG instance is used for before/after the problematic algorithm used it. The following highlights this with some example (pseudo) code:
// Assume we have an algorithm which uses an RNG, and has a bug
// which causes it to sometimes fail (return false)
public static bool AlgorithmThatSometimesFails(IEnhancedRandom rng)
{
// We simulate a failure by just returning false 50% of the time
// for this example
return rng.PercentageCheck(50f);
}
// Find an issue that occurs sometimes, and get a way to reproduce it
public static string FindAndReproduceProblem()
{
KnownSeriesRandom? sequence = null;
while (sequence == null)
{
var wrapper = new ArchivalWrapper(Random.GlobalRandom.DefaultRNG);
var result = AlgorithmThatSometimesFails(wrapper);
if (!result)
sequence = wrapper.MakeArchivedSeries();
}
// This string, when deserialized, produces a KnownSeriesRandom that will
// always produce the problem we found on the first call to
// AlgorithmThatSometimesFails
return sequence.StringSerialize();
}
You can, of course, create a similar result by simply serializing the state of the underlying generator directly before each run of the problematic algorithm, and thus avoid using KnownSeriesRandom
or ArchivalWrapper
at all; however, the resulting serialized generator will only continue to reproduce the problem so long as the implementation of the RNG used to create it does not change; the KnownSeriesRandom
approach removes the original RNG entirely from the replication process.
Namespace and Helper Method Changes
The GoRogue.Random.SingletonRandom
class that existed in GoRogue 2 has been renamed to GoRogue.Random.GlobalRandom
, to more accurately reflect its purpose. The DefaultRNG
parameter should otherwise be the same (other than now being of type IEnhancedRandom
), and will simply require updating the class name in any references.
Extension Methods for Built-In C# Types
GoRogue 2 also provided an array of extension methods for built-in C# types, which pertained to RNG in some capacity. For example, there were several extension methods defined for List<T>
, which included FisherYatesShuffle
, RandomItem
, and RandomIndex
. Similar methods were provided for other types, including arrays. All of these methods have been moved to ShaiRandom, and some have been renamed. The following lists a mapping of old names to new:
Old Name | New Name |
---|---|
FisherYatesShuffle<T> |
Shuffle<T> |
RandomItem<T> |
RandomElement<T> |
RandomIndex<T> |
RandomIndex<T> |
Additionally, these methods are now extension methods of IEnhancedRandom
, rather than being extension methods of the built-in C# type they interact with. So, whereas in GoRogue 2 you would write:
myList.FisherYatesShuffle(myRNG);
In GoRogue 3, you would write this instead:
myRNG.Shuffle(myList);
This helps to consolidate similar functions into a single namespace. Note that you will need to have using ShaiRandom;
in the code file to be able to call these methods.
Random-Oriented Methods for Other Types
GoRogue 2 also provided methods equivalent to RandomItem
and RandomIndex
, which operated on map views, rectangles, areas, and other types which were created by GoRogue. These types simply defined the function as part of the type, which made the calling syntax look very much like the extension methods for C# types:
var itemFromList = myList.RandomItem(myRNG);
var itemFromRect = myRect.RandomPosition(myRNG);
These extension methods still exist in GoRogue 3; however since the ones for built-in C# types are now extension methods for generators instead of the container type (as explained in the above section), the methods on these GoRogue defined classes have been refactored to match. So, given the above code in GoRogue 2, you would write it the following way in GoRogue 3:
var itemFromList = myRNG.RandomElement(myList);
var itemFromRect = myRNG.RandomPosition(myRect);
Note that although these extension methods are defined by GoRogue, they are still defined within the ShaiRandom.Generators
namespace, so you will need to have using ShaiRandom.Generators;
in your code files to be able to access them.
Added PercentageCheck
Additionally, a PercentageCheck
function has been added, also as an extension method of IEnhancedRandom
. This makes it much more convenient to use an RNG to perform a "percentage check" (a check with a specified percent chance to succeed) with any RNG, including DefaultRNG
.
Note that this function is quite similar to the NextBool(float chance)
method that ShaiRandom defines; however there are two key differences:
PercentageCheck
takes its parameter as a float in range [0f, 100f], whereasNextBool
accepts its percentage as a value between 0f and 1f.PercentageCheck
will throw an exception if an out-of-range percent is given, whereasNextBool
will tolerate arbitrary values.
Both behaviors can be useful depending on the situation; other than these differences, the two methods perform the same function.
Effects System Refactored
GoRogue 2 contained an "effects" system designed to help users implemented game mechanics such as damage, armor, healing, etc in an extensible way. It consisted of an Effect<T>
class and an EffectTrigger<T>
class, where the Effect<T>
class would be subclassed to implement an effect, and effect instances could then be added to an EffectTrigger<T>
instance, which represented an event and could trigger any added effects.
In GoRogue 3, this system still exists, but has been refactored to address some usability and performance concerns. The implementation in GoRogue v2 had a few main issues:
The type parameter made usage unnecessarily complex for simple use cases, and could be initially misleading to users. Primarily, this is because the type parameters given to
Effect
, and the parameters given to anEffectTrigger
it is added to, had to match; only effects that take the same parameter to theirTrigger()
functions can be added to a givenEffectTrigger
. The basic implementation having this type parameter tended to encourage using it to pass parameters that were better passed as constructor parameters to theEffect
subclass, and would get users into trouble due to type mismatches.The API was confusing for those using only
Effect
that didn't need a parameter, and without anEffectTrigger
, because the parameter was completely useless.The type used for the type parameter needed to be a reference type. This has a number of performance implications due to the allocation of new types constantly.
These concerns have been addressed in GoRogue 3 via the following changes.
Effects Namespace
First, all effect-related classes have been moved to the GoRogue.Effects
namespace. This on its own is a relatively minor change, and is mostly a side effect of there being more classes related to effects in the new implementation.
Effect and EffectTrigger Do Not Take Type Parameters
The biggest change, is that Effect
and EffectTrigger
no longer take any type parameters. Instead, Effect.Trigger
and Effect.OnTrigger
take an out bool
parameter, which you set to true
if you want to cancel a trigger. This is the equivalent of setting args.CancelTrigger
to true
in the old implementation. EffectTrigger.TriggerEffects
doesn't take any parameter at all; instead, it creates a boolean internally and passes it as the out bool
parameter to Trigger
.
Additionally, there is an overload of Effect.Trigger
which takes no parameter at all. This is simply meant as a convenience for when you're calling Trigger
manually for an instantaneous effect and don't care about the parameter value.
This change removes the ability for you to pass custom parameters to Effect.OnTrigger
. If you do need to do so, you will instead need to use the new classes introduced into GoRogue.Effects
; AdvancedEffect<TTriggerArgs>
, and AdvancedEffectTrigger<TTriggerArgs>
. These classes are identical to Effect
and EffectTrigger
, respectively, except that they take an additional parameter to their trigger-related functions of type TTriggerArgs
. This is functionally equivalent to Effect<T>
and EffectTrigger<T>
from GoRogue 2; except that the TTriggerArgs
type can be of any type (value or reference type), and therefore the out bool
parameter still exists to allow for cancellation.
Examples that use both Effect
/EffectTrigger
and AdvancedEffect<TTriggerArgs>
/AdvancedEffectTrigger<TTriggerArgs>
can be found in the effects system how-to article.