Skip to content

Instantly share code, notes, and snippets.

@vsapsai
Last active October 29, 2019 05:17
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save vsapsai/6f524c5095a7ae647f1746c762954f9f to your computer and use it in GitHub Desktop.
Notes on the talk Designing and Evaluating Reusable Components by Casey Muratori. View in raw.

Notes on the talk Designing and Evaluating Reusable Components by Casey Muratori. https://caseymuratori.com/blog_0024

Slides are separated with ---, my comments are after ~~~.


Designing and Evaluating Reusable Components

Casey Muratori

casey@mollyrocket.com


Code reuse. Sigh.


Layer

+-------------------------+
| |
| New |
| |
+----+--------------+-----+
‖ |
V |
+----------+ |
| | |
| Reused | |
| | |
+----+-----+ |
‖ |
V V
+-----------+ +-----------+
| | | |
| Service | | Service |
| | | |
+-----------+ +-----------+

Engine

+-------------------------+
| |
| Reused |
| |
+---+-----------+---+-----+
‖ ^ | |
V ‖ | |
+------+---+ | |
| | | |
| New | | |
| | | |
+----------+ | |
+--------+ |
V V
+-----------+ +-----------+
| | | |
| Service | | Service |
| | | |
+-----------+ +-----------+

Component

+-------------------------+
| |
| New |
| |
+---+-----------+---+-----+
‖ ^ | |
V ‖ | |
+------+---+ | |
| | | |
| Reused | | |
| | | |
+----------+ | |
+--------+ |
V V
+-----------+ +-----------+
| | | |
| Service | | Service |
| | | |
+-----------+ +-----------+

Three types of reuse

~ ~ ~ An example of Layer is OpenGL, it works with GPU directly. With Engine you write a small part that should conform to existing rules. Component is a physics library, for example, as it has stable output that you have to consume in a useful way.


Layers require services (standards)

+-------------------------+
| |
| Game |
| |
+-----------+-------------+
|
V
+----------+
| |
| Layer |
| |
+----+-----+

V
+-----------+
| |
| ?? |
| |
+-----------+

Layers can conflict

+-------------------------+
| |
| Game |
| |
+---------+-----+---------+
/ \
V V
+----------+ +----------+
| | | |
| Layer | | Layer |
| | | |
+------+---+ +-+--------+
‖ ‖
V V
+-----------+
| |
| Service |
| |
+-----------+

Layers alone are insufficient

There are cases where layers don't work.

---

Service not required

+-------------------------+                    
|                         |                    
|          Game           |                    
|                         |                    
+----------+--------------+                    
           |   ^                                     
           V   |                                     
       +-------+---+                                   
       |           |                                   
       | Component |                                   
       |           |                                   
       +-----------+                                   

Conflicts resolved in game

+-------------------------+                    
|                         |                    
|          Game           +------+             
|                         |      |             
+-----+------------+------+      |             
      |   ^        |   ^         |              
      V   |        V   |         |                   
  +-------+---+  +-----+-----+   |             
  |           |  |           |   |             
  | Component |  | Component |   |             
  |           |  |           |   |             
  +-----------+  +-----------+   |             
                                 |                 
        +-----------+            |                     
        |           |            |                     
        |  Service  |<-----------+                     
        |           |                                  
        +-----------+               

Components provide solution

One can solve previously mentioned problems with components, where problems with services are avoided.


Components solve most remaining reuse problems, but are significantly harder to design.


+-------------------------+
| |
| Game |
| |
+----------+--------------+
| ^
V ‖
+-------+---+
| |
| Component |
| |
+-----------+

Components are integral

The main complexity is in the backchannel from the component to the game.
Following are different ways how games are integrating with components. Mostly
applicable to the games, might be applicable to other software too.

---

// Won't reproduce the graph but you have bumps in integration work prior to
// initial bringup, right before the major demo, right before the first beta.

Integration grows in spurts

---

// Scatterplot graph, on x axis is Benefit to Game, on y - Integration Work.
// In general, to get more benefit, you need to do more work.

Integration options

At first you have Minimum Initial Feature Set where you aim to get the most benefit with the small amount of work.


Integration progression

Over the duration of the development you have New Requirements that require
more Integration Work to bring more Benefit to Game.
But sometimes it can turn out there is a huge jump between development states
and to get more Benefit you need to do a lot of Integration Work.

---

* Way overkill               * Way overkill
===================          =================
* Slightly overkill

* Solved
===================          =================
* Unsolved                   * Unsolved

APIs can force large steps

---

* State B


* Integration discontinuity

===========================
* State A

Integration discontinuity

---

// A graph shows you are doing more work than getting benefit.

Discontinuities waste work

Sometimes you are trying approaches that don't pan out and need to redo previous work which causes wasted work.


The primary goal of component API design is to eliminate API discontinuities.


Current API design trends

Latest trend in API design is to limit API as much as possible, so you don't
have multiple integration options.

---

Granularity - A or BC
Redundancy - A or B
Coupling - A implies B
Retention - A mirrors B
Flow Control - A invokes B

Five Characteristics

Granularity allows to split an operation into smaller steps giving you more control. Redundancy is having slightly different API for the same purpose. Coupling means there is hidden requirement that you need to satisfy and cannot avoid.


A UpdateOrientation(Object);

B Orientation = GetOrientation(Object); Change = GetOrientationChange(Object); SetOrientation(Object, Orientation + Change);

C Orientation = GetOrientation(Object); Change = GetOrientationChange(Object); Change += 3.14f; // TODO: Close enough to Pi? SetOrientation(Object, Orientation + Change);

D Orientation = GetOrientation(Object); Change = GetOrientationChange(Object); RunSomeOtherUnrelatedThing(); SetOrientation(Object, Orientation + Change);

Granularity - A or BC


A SetOrientation3x3(Object, Matrix);

B SetOrientationQ(Object, Quaternion);

C IdentityOrientation(Object); FaceForwards(Object);

D OrientInDirection(Object, Vector); OrientTowards(Object, Point);

E NewOrient = GetOrientAndChange(Object); SetOrientation(Object, NewOrient);

F Orient = GetOrientation(Object); SetOrientDelta(Object, Orient, Change);

Redundancy - A or B


A UpdateEverything(Universe);

B SetTime(GlobalTime); UpdateObject(Object);

C BeginObjectSpecification(); Object = EndObjectSpecification();

D String1 = GetMungedName(Name1); String2 = GetMungedName(Name2);

E Object = AllocateAndInitialize();

F Matrix = MakeMatrixFrom(FloatPointer); SetOrientationM(Object, Matrix);

G Object = ReadObject(Filename);

Coupling - A implies B


A SetTime(GlobalTime); SetPi(3.14f); // TODO: Close enough to Pi?

B SetParent(ChildObject, ParentObject); UpdateOrientation(ChildObject);

D SetFileCallbacks(Open, Read, Close); File = OpenFile(Filename);

Retention - A mirrors B


A LibTestNoodleWidgetHit LibProcessNoodleWidget GameProcessWidgets GameUpdate

B GameHandleNoodleWidgetHit LibTestNoodleWidgetHit LibProcessNoodleWidget GameProcessWidgets GameUpdate

C LibNoodleWidgetChangeHeight GameHandleNoodleWidgetHit LibTestNoodleWidgetHit LibProcessNoodleWidget GameProcessWidgets GameUpdate

Flow Control - A invokes B


Call stacks grows from bottom to top - game code is on the bottom, component
code is on the top.

---

A File = OpenFile(Filename);

B SetFileCallbacks(Open, Read, Close);
  File = OpenFile(Filename);

C class my_handle : public file_handle
  {
  public:
    virtual void Open(char *Filename);
  };

D throw 3.14f;  // TODO: stop using exceptions

Flow Control - A invokes B

---

Granularity - A or BC
  Flexibility vs. simplicity

Redundancy - A or B
  Convenience vs. orthogonality

Coupling - A implies B
  Less is always better

Retention - A equals B
  Synchronization vs. automation

Flow Control - A invokes B
  More game control is always better

Recap

---

// At Initial stage you want Low granularity, High retention
// At First Beta you want Hight granularity, Low retention

Tradeoff decisions often vary

---

The following examples are based on real-life stories of real
game developers in dangerous development situations.

---

Function names have been changed to protect the
innocent / guilty.

---

A Thing = ReadFile(Filename);

B SetFileCallbacks(Open, Read, Close);
  Thing = ReadFile(Filename);

C Thing = MakeThingFromData(FileData);

Game-provided services

A big problem here (especially with B) is mixing 2 tasks: reading from disk and creating Thing. C is a more desirable approach.


C Thing = MakeThingFromData(FileData);

D FileData = DecompressFile(RawFileData); Thing = MakeThingFromData(FileData); FreeFileData(FileData);

E SetMemoryCallbacks(Allocate, Deallocate); FileData = DecompressFile(RawFileData); Thing = MakeThingFromData(FileData);

F Size = GetProcessedSize(RawFileData); FileData = malloc(Size); DecompressFile(RawFileData); Thing = MakeThingFromData(FileData);

Game-provided services

Let's assume you are dealing with compressed data and want to decouple Thing
creation from decompressing. Examples C-F are gradual refinement of API to
decouple these tasks properly.

---

F Size = GetProcessedSize(RawFileData);
  FileData = malloc(Size);
  DecompressFile(RawFileData);
  Thing = MakeThingFromData(FileData);

G Thing = NewThing();
  Read(File, sizeof(Thing), &Thing);

H Thing = AllocateInAGP(sizeof(Thing));
  Read(File, sizeof(Thing), &Thing);

Game-provided services

At different stages of game development you want different things. At some point you might prefer A and at some point - H.


A Inverse = InverseTransform(Xform);

B Xform = TransformFrom(Position, Rotation); Inverse = InverseTransform(Xform); CopyTransform(Inverse, Position, Rotation);

C GetPosition(GameXform, Position); GetRotation(GameXform, Rotation); Xform = TransformFrom(Position, Rotation); Inverse = InverseTransform(Xform); CopyTransform(Xform, Position, Rotation); SetPosition(GameXform, Position); SetRotation(GameXform, Rotation);

D InverseTransformQ(Position, Rotation, Position, Rotation);

Parameter redundancy

C shows ridiculous code required when your data structure for coordinates is
not the same as in a component. And option D is a better API.

---

A UpdateNode(Node);
  RenderNode(Node);

B // Update
  Rotate(XForm, t * RadiansPerSecond);
  Translate(XForm, t * MetersPerSecond);
  WorldMesh = BuildWorldState(Mesh, ParentMesh);

  // Render
  MaterialSort = NewMaterialSort();
  Sort(MaterialSort, WorldMesh);
  RenderOpaque(WorldMesh, XForm, MaterialSort);
  RenderAlpha(WorldMesh, XForm, MaterialSort);
  ReleaseMaterialSort(MaterialSort);

Granularity transitions

In this example you have A but want to change how a node is updated. But now you need to implement render as well which is not ideal.


C // Update Rotate(XForm, t * RadiansPerSecond); Translate(XForm, t * MetersPerSecond); WorldMesh = BuildWorldState(Mesh, ParentMesh);

// Render Render(WorldMesh, XForm);

D // Update Rotate(XForm, t * RadiansPerSecond); Translate(XForm, t * MetersPerSecond); WorldMesh = BuildWorldState(Mesh, ParentMesh);

// Render MaterialSort = NewMaterialSort(); RenderSorted(WorldMesh, XForm, MaterialSort); ReleaseMaterialSort(MaterialSort);

Granularity transitions

C and D offer different levels of granularity that can be beneficial.

---

A Rocket = CreateRigidBody();
  Pole = CreateRigidBody();
  Hookline = CreateJoint(Rocket, Pole);
  Simulate();

B if(XButtonDown)
  {
    if(!Hookline)
    {
      Hookline = CreateJoint(Rocket, Pole);
    }
  }
  else if(Hookline)
  {
    DeleteJoint(Hookline);
    Hookline = 0;
  }
  Simulate();

Retention mismatch

Component authors might expect you to write code like A. But in real games you have code more like B.


B if(XButtonDown) { if(!Hookline) { Hookline = CreateJoint(Rocket, Pole); } } else if(Hookline) { DeleteJoint(Hookline); Hookline = 0; } Simulate();

C if(XButtonDown) DoJoint(Rocket, Pole); Simulate();

Retention mismatch

The desired API is C. C is an immediate mode, A is a retained mode.

---

Optimizing the five characteristics leads to an API
which is gradually tiered, highly decoupled,
has no retention at its most granular tier,
and always lets the game dictate the flow of control.

---

And now, the crib sheet.

---

* The first thing you did was write the code that uses the API.

* The second thing you did was write the code that uses the API.

API evaluation checklist

When evaluating API from different vendors, try integrating with your game first, don't check vendors' documentation. Think about API in your terms, not in their terms.


  • All retained mode constructs have immediate-mode equivalents.

  • For every API that uses callbacks or inheritance, there is an equivalent API that does neither.

  • No API requires the use of a API-specific datatype for which the average game already has an equivalent.

  • Any API function your game may not consider atomic can be re-written using between 2 and 4 more granular APIs (not counting accessors)

API evaluation checklist


  • Any data which does not clearly have a reason for being opaque should be transparent in all possible ways (construction, access, I/O, etc.)

  • Use of the component's resource management (memory, file, string, etc.) is completely optional.

  • Use of the component's file format is completely optional.

  • Full run-time source code is available.

API evaluation checklist


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment