Skip to content

Changing properties that are records (like vectors in Castle Game Engine)

Michalis Kamburelis edited this page Aug 22, 2023 · 10 revisions

(See also: https://castle-engine.io/coding_traps )

Castle Game Engine defines various record types. For example TVector3 (and other TVectorXxx), TMatrixXxx, TCastleColor and TCastleColorRGB (colors are actually just aliases to TVector3 or TVector4).

Various classes have properties of record types. On this page we use, as an example, the TCastleTransform.Translation property, of type TVector3. This is a very often used property, it moves an object (3D or 2D, of TCastleTransform or TCastleScene class).

Why Use Records (in some cases)?

We like record types for some reasons:

  • simple assignment of records (A := B) copies the value (not a reference/pointer to it), which is more natural in some cases.

    E.g. you just add records like A := B + C, you don't need to worry about any memory management or references, you operate on values.

  • records have a clear layout in memory (e.g. you can pass TVector3 directly and reliably to an external library like OpenGL),

  • an array of records is a one tight block in memory (which is good for cache, in case you e.g. want to sum all the vectors).

Do not take this as an advice to use records for everything. In fact, in most cases, designing your API using classes (not records) should be your first choice. Classes have clearly defined constructors, destructors, virtual methods, and more, and these features are really helpful to properly manage the memory of complicated interconnected structures. The point is only: there are sometimes valid reasons for using records for some stuff -- like vectors in a game engine.

How To Modify Record Properties

Remember to always set these records as a whole, not just modify their components. For example this is correct:

MyScene.Translation := MyScene.Translation + Vector3(10, 0, 0);

This is correct also:

var
  V: TVector3;
begin
  V := MyScene.Translation;
  V.X := V.X + 10;
  MyScene.Translation := V;
end;

How To Design Record Properties

We have prevented the incorrect usage of TVector3 records in new CGE versions, see https://castle-engine.io/coding_traps .

This was done by never having a property / method in TVector3 that sets some record field. The only way to set record fields is now using direct field access. This prevents the problems outlined below (compiler will not let you write invalid code).

How To NEVER Design and Use Record Properties (because it may fail in non-obvious ways)

Never do this:

  1. Declare a record with a property that allows to set the field (using a setter method or direct field access). E.g. this:
TBadVector3 = record
  X, Y, Z: Single;
  { Alias for X. }
  property AlternativeNameForX: Single read GetX write SetX;
  { Alias for X. }
  property AlternativeNameForY: Single read GetY write SetY;
  { Alias for Z. }
  property AlternativeNameForZ: Single read GetZ write SetZ;
end;
  1. Then modify the record properties like this:
// THIS IS INCORRECT:
MyScene.Translation.AlternativeNameForX := MyScene.Translation.AlternativeNameForX + 10;

// This line is (fortunately) a compiler error:
// MyScene.Translation.X := MyScene.Translation.X + 10; 

Depending on the definition of TCastleScene.Translation, the MyScene.Translation.AlternativeNameForX := ... assignment could lead to various behaviors:

  1. If Translation is a property with a getter method (property Translation read GetTranslation write SetTranslation), it does nothing.

    That's because it's equivalent to MyScene.GetTranslation.AlternativeNameForX := MyScene.GetTranslation.AlternativeNameForX + 10;, i.e. you just modify the value of a temporary record returned by a method.

    Note that C# structures have the same trap.

  2. If Translation is a property with a direct field to read (property Translation read FTranslation write SetTranslation), it modifies the underlying field but without calling the SetTranslation method.

    It's equivalent to MyScene.FTranslation.X := MyScene.FTranslation.X + 10;, i.e. you just modify the field, bypassing the setter. This can lead to various surprising consequences. E.g. in case of TCastleScene.Translation, it means that physics engine will not be notified about the change, and TCastleScene.WorldTransform will also not be updated.

    TODO: Wasn't this just a bug in FPC later fixed? Test.

  3. Only when Translation is a simple field (Translation: TVector3, not a property), it will work as expected. There is no getter or setter method in this case, of course.

In each CGE release, the definition of TCastleScene.Translation may change. In fact it already happened (in CGE <= 6.4, we had 1st case, read GetTranslation; in CGE >= 6.5, we have 2nd case, read FTranslation). These changes are sometimes necessary (sometimes we introduce a "getter" method when the underlying implementation changes, and a getter is needed e.g. to get a value from internal instance of something, or to cache some calculations; sometimes you remove a "getter" method when the underlying implementation no longer needs it, and you want to optimize reading this property).

So, you are only safe if you always set the translation like MyScene.Translation := ....