Skip to content

Archive

Tag: Software Design

In a project involving the creation of a new software library, the architect’s role is somehow similar to that of a product manager. The product is the library, and the architect may be in charge of maintaining the vision associated with this product. Reflecting on personal experience, this article exposes what an architect may expect while collaborating within a moderately sized team. I have found that, should one or more of these elements be missing, waste may occur, and product integrity may be compromised. Here’s the list.

1. Trust and Respect

The architect is in charge of the product’s vision – the API or library is the product. Other team members should trust and respect the architect’s work, assuming the following to be true:

  • (a) The architect has sufficient expertise in the target domain to formulate reasonable propositions.
  • (b) The architect has much more time than the rest of the team to consider, and direct the team towards, short to long term API development strategies.
  • (c) The architect is defining functionality that will be at least attractive, and usually valid, from the API user’s point of view(1).

If a team member feels that (a) or (c) can be disproven, then they should raise the matter with their team lead or technical director.

If a team member finds that (b) is untrue, they should try to support the architect better, so they find time to do their job (see support)

2. Understanding good design

Good design doesn’t require locally perfect APIs, nor even striving towards a hypothetical, holistically perfect API.

Good design means creating an API with the following qualities:

  • Fitness for purpose - If the API can be successfully used to do the job it is intended to, local flaws matter little.
  • Consistency - It is often better to strive for consistency than to strive for local perfection.
  • Timely delivery - While this may not always be critical, there are often operational goals associated with the development of an API.

Maybe the most essential point about good design is that there are always several, roughly equivalent ways to design a fit for purpose API. Further, few APIs partaking a fit for purpose library, if any, will be ‘locally perfect’.

Consistency requires giving up a little perfection here and there. Timely delivery occasionally requires taking shortcuts, deferring a re-factoring, or even endorsing what will ultimately prove to be a design of significantly lesser quality. Neither needs be a concern as long as fitness for purpose is not affected.

3. Support

If team members understand good design and trust the architect – or at least, respect their expertise – they are ready to support API development. What does this mean?

To support API development means to participate in a team effort promoting good design, versus promoting personal twists and quirks. Here are a few things that a good team member will do:

  • Review the design either with trust and respect, or with a degree of domain expertise matching that of the architect. If a team member isn’t interested in the overall design or doesn’t understand the product vision, they do not qualify as a peer (2), and therefore should defer to the architect’s design decisions.
  • Situate implementation related concerns within a wider scope. A team member should discuss implementation related problems only insofar as such problems may invalidate the proposed API, e.g. by making realizing the desired functionality impossible (logical flaw) or intractable (lack of time), or by significantly limiting the performance of the resulting API (or that of a product dependent on such API).
    A team member should not expose or discuss all implementation details to/with their architect. The architect won’t be able to do a good job if they spend their time learning and analyzing such or such detail versus designing the API and maintaining the development strategy.
  • Respect the API coding style and semantics. Stylistic matters… …don’t matter. Consistency in naming APIs, whether using explicit or implicit policies, is, however, essential. Discuss stylistic matters at the beginning of the project if you can’t help it. Learn policies, either by reading them or looking at the specs, if you’re joining an existing project.
  • Propose improvements, not cancellations – APIS are means to ends. Suggesting that an API isn’t useful may be OK if a team member genuinely understands the product vision and the library’s design goals. Otherwise, the architect should legitimately feel disappointed that they get no support from their team member.
  • Attempt not to overload APIs – an API is built atop other APIs. The target product isn’t the sum of all its components. It is intended as a domain specific, enabling technology. If the API omits to set such or such parameter, it is quite possible that the implementation is expected to take care of this on a best effort basis – just forwarding additional, unrequited parameters risks exposing details better hidden from the API user.
  • Know when is the time to discuss APIs – Whenever work pressure allows it, the architect will discuss both details and general orientations with other staff, within and without the team. The architect may be trying to assert feasibility, be concerned about timely delivery or just looking for advice; conversely, team members may feel that they have concerns about the API at any thinkable level. So when is the time to discuss APIs?
    • Discuss once and only once. Moving forward requires ‘moving in the same direction for a while’ . Discussing things over and over reduces the chances that the library will be delivered on time.
    • Re-discuss when and as new information is being uncovered. Agile API design is incremental. New information is likely to be discovered  during this process. If a team member encounters new information that significantly questions previous choices, they must expose relevant evidence.
      Ways to discover new information may include writing proofs of concept, deep thinking, testing APIs with code that uses them and other methods.
  • Endorse and implement design decisions. Ultimately, individual team members may or may not feel happy about design decisions. So if a team member perceives a decision to be flawed against the architect’s or the rest of the team’s opinion, what should they do (see below for more options)? They should go ahead, endorse and implement either the requested design, or at least a proof of concept. libraries are code. Prove or disprove a design using running code rather than re-discussing issues over and over. Everybody makes mistakes; everybody is sometimes right against everybody else. The burden of proof is on running code.
  • Commonsense. Unless an architect is extremely methodic and has a lot of time on their hands, they are likely to miss out details while drafting APIs. The API designer’s goal is to communicate enough about the APIs that they can be implemented by a supportive team endowed with commonsense. The architect will expect an implementer to fill in the details, not stare at the holes.
  • Awareness. A team member should strive to keep pace with the developing API, not just look at things from their own, narrow end. Awareness allows a team member to support design by refining their understanding of the emerging product. Without awareness, a team member will neither acquire, nor retain peer status, therefore may no longer contribute to design discussions, and may misunderstand the intent behind APIs pushed to them.
    Even if a team member does not wish to involve in the design process, they should keep a reasonable degree of awareness. In this case, consider awareness a natural extension of commonsense.

4. Collaboration

Within an API development team, collaboration means at least two separate things:

  • Help each other work faster by learning each other’s code and promoting code reuse.
    Share code; share advice; share knowledge. When an API becomes large enough, opportunities for sharing and reusing code naturally multiply. While the architect may anticipate such opportunities, modeling dependencies internal to an API implementation is not the primary role of the API designer. Team players quickly detect opportunities for reuse, for example by reviewing each other’s code.
  • Non-competition. The team must strive to build the library together. There are various ways in which team members can drift away from this goal.
    Team members may develop frustration over time because the wider goals of the API and library development process stifle their individual creativity.
    Such or such team member may favor a utility library over the team’s choice…
    Team members may develop an alternative vision while developing awareness…
    Non competition means that, before asking oneself ‘Can I do this better than what the spec/implementation design says?‘, a team member should ask themselves: ‘Will my choice promote our success‘. A team member may feel that deviating from the team’s choices and re-writing existing libraries signals excellence. Unfortunately, the better is usually the enemy of the good in this case. Even moderately large libraries require canvassing and compromises.

5. Knowing how to say NO

Finally, what should a team member do when they disagree with the team and/or the architect?

  1. Disagree, once and only once – Team members should explain why they disagree. This may often help shift or correct the team’s decision. Once and only once also means that, once a general direction has been agreed, it is not OK to re-discuss this orientation while dealing with every instance of the general case.
  2. Apply deference and trust. This is a simple rule that may be more difficult to apply from a team lead’s point of view. If the team lead and the architect are not the same person, then the team lead should normally defer to the architect, unless they combine peer status with authority.
    Whenever the team lead overrides or denigrates the architect’s decisions (beyond seeking consensus), they are practically undermining the architect’s role. This can be OK from time to time if the team lead is actually right. Otherwise – or simply if it’s impossible to figure what’s right or wrong – this will likely lead to the architect being forced to step down. Why? Not only because this makes it hard for the architect to do their job, but also because, from the architect’s point of view, continued disagreements with the team lead can lead to managerial action against the architect.
  3. Get help from above - Get help from somebody who cumulates peer status and, roughly speaking, experience above or at the same level as, the architect’s. Getting help from above is only meaningful if a team member feels that they are addressing a critical / major issue that will compromise ‘good design’ as described in (2). Not everybody senior and experienced considers clearing misunderstandings and nit-picking as a valuable use of their time.
    The architect is not mandated to validate all of their decisions with a senior, more experienced staff. First of all there might not be such a person around. Should there be, the architect won’t be doing their job if they have to routinely rely on external advice. Regularly asking your architect if they have discussed this and that with such and such senior staff evaluates to questioning their competence.
  4. Avoid sarcasm, irony and bad omens. Agreeing to something while promising it will lead to disaster or waste is not just promoting bad mood in the team, it is also irresponsible. If a team member has serious issues with the team or the architect’s decisions, they should appeal to a senior (e.g., technical director) and, ultimately, defer to their advice.

Right – I don’t think this stuff is too original, and I suspect a lot of it applies to team work in general, not just API design or the architect’s role. If you’re having trouble understanding what’s going on in your team and suspect there are little grains of sand between the cogs and wheels, I hope this helps.

(1) Depending on the library’s target audience, different constraints may define what is an attractive or valid API. Writing an API for a casual PHP developer does not generate the same constraints as writing an API for financial banking. What is essential to financial banking programmers may be unthinkable for casual script writers.

(2) Several points in this article depend on understanding so called ‘peer status’. Peer status (in short, having expertise relevant to the design problems underlying API design) is not a global property of a team member. Peer status is more like a variable – Whether a team member is competent to address a design problem should often be considered on a case basis.

In this article, I’m looking at two distinct things:

  • Basic use of AVAudioPlayer, a CocoaTouch class that gives reasonably good sound support for many applications. If you’re deep into waves, search OpenAL instead.
  • How to use the F* meta-pattern to integrate audio as a ‘pluggable’ feature, with minimum interaction with existing code.

As a side effect (this is integral to the spirit of the F* pattern), you might be able to integrate sample code from this article in your application very easily. If you’re not into the theory, please skip to the code and have fun.

A few days ago, I have decided to try applying the F* meta-pattern with my game integration. A first step towards this was i/o integration, for which I provided:

  • A dispatcher for in-game milestone events
  • A protocol used to identify consumers of milestone events.
  • Classes packaged separately that take care of i/o.

Practice makes better. I’m starting to understand how I can apply F* to developing my game, and audio integration is the next step.

Here, I want to associate a sound with specified actor activities. For now I’ll keep it simple, just start a sound when the actor begins an activity.

In my little game framework, the actor’s activity is updated at every frame. Activity changes only occur when the new activity is different from the previous activity. So what I’ll do is define an observer pattern allowing me to detect activity changes without creating type dependencies with the Actor class. I’ll then register an instance of a ‘sound feature class’ with the event dispatcher.

Observer Pattern

I have moved my dispatchers and listeners to a separate package. I then create a new group including the following:

  • ActivityChangeDispatcher provides static methods used to notify listeners to activity change events.
  • ActivityChangeListener specifies… activity change listener functions.

Here is the code for ActivityChangeListener (act is just a typedef in Definitions.h):

#import "Definitions.h"
@class Actor;
@protocol ActivityChangeListener
-(void)actor:(Actor*)a activityChangedFrom:(act)a0 to:(act)a1;
@end

Then the header file for ActivityChangeDispatcher

#import "Definitions.h"
#import "ActivityChangeListener.h"
#import "EventDispatcherMacros.h"
@class Actor;
@interface ActivityChangeDispatcher : NSObject
DEC_LISTENERS(ActivityChangeListener)
+(void)actor:(Actor*)a activityChangedFrom:(act)a0 to:(act)a1;
@end

Finally the implementation of ActivityChangeDispatcher.

#import "ActivityChangeDispatcher.h"
@implementation ActivityChangeDispatcher
DEF_LISTENERS(ActivityChangeListener)
+(void)actor:(Actor*)a activityChangedFrom:(act)a0 to:(act)a1{
	for (id l in listeners) {
		[l actor:a activityChangedFrom:a0 to:a1];
	}
}
@end

DEC_LISTENER() and DEF_LISTENER() are declared as follows in EventDispatcherMacros.h:

#define DEF_LISTENERS(interface)								\
static NSMutableArray* listeners;								\
+(void)addListener:(id)l{							\
	if (listeners==nil)listeners=[[NSMutableArray alloc]init];		\
		[listeners addObject:l];					\
}														\
+(void)removeListener:(id)l{							\
	if (listeners==nil)return;						\
	[listeners removeObject:l];						\
}																

#define DEC_LISTENERS(interface)						\
+(void)addListener:(id)l;							\
+(void)removeListener:(id)l;

I was kind of hoping I might be able to define a catch-all macro avoiding writing code for new observer pattern classes altogether. This isn’t quite like it although some of the boiler plate has been removed. But then I can probably do better. Next time i’ll try to make this more concise as I do not want to define more than one event per dispatcher.

Once this is done, I need to activate the dispatch by pasting this code wherever activity changes:

[ActivityChangeDispatcher actor:a activityChangedFrom:x to:y];

The Sound Feature

My sound feature will be PlaySoundOnActivityChange.

Here’s the header

#import "ActivityChangeListener.h"
#import "Definitions.h"

@interface PlaySoundOnActivityChange : NSObject  {
	NSMutableDictionary* map;
	NSMutableDictionary* players;
}
-(void)map:(tag)label to:(tag)soundFile;
@end

Now here’s the implementation:

#import "PlaySoundOnActivityChange.h"
#import <AVFoundation/AVFoundation.h>
#import "ActivityChangeDispatcher.h"

@interface PlaySoundOnActivityChange (private)
-(AVAudioPlayer*)prepareSound:(tag)name;
@end

@implementation PlaySoundOnActivityChange
-(id)init{
	if (self=[super init]) {
		map=[[NSMutableDictionary alloc]init];
		players=[[NSMutableDictionary alloc]init];
		[ActivityChangeDispatcher addListener:self];
	}return self;
}
-(void)actor:(Actor *)a activityChangedFrom:(act)a0 to:(act)a1{
	NSString* code=[NSString stringWithFormat:@"%i",a1];
	tag name=[map objectForKey:code];
	if (name==nil)return;
	AVAudioPlayer* player=[players objectForKey:name];
	if (player==nil)player=[self prepareSound:name];
	if (player==nil)return;
	[player play];
}

-(void)map:(tag)label to:(tag)soundFile{
	[map setObject:soundFile forKey:label];
}
// private ---------------------------------------------------------
-(AVAudioPlayer*)prepareSound:(tag)name{
	NSBundle	*mainBundle = [NSBundle mainBundle];
	NSError		*error;
	NSURL		*url = [NSURL fileURLWithPath:
[mainBundle pathForResource:name ofType:@"caf"]];
	//
	AVAudioPlayer* player = [[AVAudioPlayer alloc]
initWithContentsOfURL:url error:&error];
	if (!player) {
		NSLog(@"no player: %@", [error localizedDescription]);
		return nil;
	}
	[player prepareToPlay];
	[players setObject:player forKey:name];
	return player;
}
@end

I haven’t tested this yet (OK, it compiles… – PS: after testing, it actually works). Also, I’m a little bit worried about what will happen if I have a 100 or a 1000 sounds registered in my map – I don’t really know what resources players are holding to, especially once [prepareToPlay] has been invoked. Pending, note the way a feature is designed and implemented using F*, and also a couple of errors:

  • I have provided an event dispatcher allowing me to trigger sounds at the right moment. The generated events provide me with all the information I need to select sound.
  • Actor does not import PlaySoundOnActivityChange – I can remove the sound feature without breaking my code (so far).
  • PlaySoundOnActivityChange and ActivityChangeListener/Dispatcher import the actor class. This could, and should, be easily avoided (so that I can use the same sound feature for non actor sounds, or in an altogether different context.

I need to import sounds into my project and map them. Probably this would be done better with a property list defined as a resource but for now, I just want to map a couple of values and check the result. So I define a feature setup class with a couple of mappings. Invoke the setup class directly from my GameSetup class. Here’s the code for InitSound:

#import "InitSound.h"
#import "PlaySoundOnActivityChange.h"
#import "Definitions.h"
#import "VRConstants.h"

@implementation InitSound
+(void)apply{
	NSString* label1=[NSString stringWithFormat:@"%i",WALK];
	NSString* label2=[NSString stringWithFormat:@"%i",STAB];
	PlaySoundOnActivityChange* feature=[[PlaySoundOnActivityChange alloc]init];
	[feature map:label1 to:@"tick"];  // just reused the sounds
	[feature map:label2 to:@"tock"]; // from Metronome example for a try.
}
@end

Done with sound.

This is my third post on programming NPC behavior in a prototype game engine. I summarize my design (it’s working!). Next I’ll look into other features I might want to add to the prototype.

Summary of design so far

The Script class provides methods to setup a script using simple, statement-like function calls. The Script class also realizes a data structure for the resulting script. Classes:

  • Script
  • Type
  • ScriptDef
    • Selector
    • Statement

The Interpreter class binds a script to a scene by setting up an action graph

An action graph is defined as a hierarchy of actions and conditions. The root of the action graph (typically a Transition instance) is invoked at every model update. Classes used in the action graph:

  • Action – defines the apply method
    • Transition – a simple action group
      • XTransition – extends transition with the ability to replace an action by an alternative if failed, or a next action if complete.
    • PrepareActors
    • BasicActorUpdates
    • XAction – adds the (BOOL)failed and (BOOL)completed methods along with definition for alternative/subsequent action
      • ReflexMap – Select and apply the first action that matches an associate trigger condition
      • Decision – Base class for actor actions (all decisions define an agent)
        • Do – general purpose action (these actions typically match to a sprite animation)
        • LookAt – make an actor look at another actor
        • Approach – come closer to another actor
        • Follow – come closer to an actor if not within a given threshold
        • Avoid – go away from an actor if within a given threshold
  • Condition – defines the (BOOL)check method; used in reflex maps
    • NearestTarget – evaluate the nearest target given an agent and a list of actors
  • TargetSelector – a complex action used to select one or several actors based on several criteria (min/max distance, actor tags, actor activity)

In my tests, I create an XTransition instance as root (this is invoked by an NSTimer) and add the following nodes in order:

  1. PrepareActors. This is used to clear some actor parameters. In my first tests, the speed vector associated with an actor wasn’t cleared. This would be OK if speed was redefined every time, but some actions (e.g: Avoid) are modifiers rather than setters.
  2. An XTransition node, used to hold actor decisions.
  3. An instance of BasicActorUpdates. BasicActorUpdates allows resolving conflicting decisions; for example, BasicActorUpdates prevents actors from entering a solid cell.

I have tested this design with a simple script and (sure, after some debugging) it works quite well. I’ll try to post a picture or video later on.

Main design features

  • The action graph separates game behavior from game data. It also allows specifying complex behaviors at runtime by adding and removing nodes. At compile time, the action graph requires the data model.
  • Tags are string or integer labels used to identify actors indirectly (used in place of data pointers). This allows defining both class and character specific behaviors.
  • Scripts are defined independently from either the action graph or the scene. Scripts have no compile time dependencies with either the data model or the action graph, so they allow simple, highly portable game behavior definitions – An interpreter is used to bind a script to an actual action graph and scene.
  • Separate decision and action. This is not required by the API, but simplifies behavior programming a lot. First all actors evaluate ‘what they would like to do’, next interactions are resolved locally, taking conflicts into account.

Recently, I started designing a game engine with a few simple goals in mind:

  1. My game engine is concerned with managing a virtual world model. It is not an animation engine or a sequence/transition manager.
  2. Although I developed a rough and ready animation API, the model must be separate from that API. This is so that I can later rewrite view using a 3D engine or whatever else.

I’ve been muddling a little bit with behavior programming. After ‘going dark’ for a few days I’ve decided to share my experience as I believe this could illuminate some design points.

Behavior programming – a draft API

A few important classes from the draft API:

  • Condition – a condition is an object that returns a boolean value, so it has a (BOOL)check method.
  • Action – an action is an object with a (void)apply method.
  • ReflexMap – an action mapping a trigger (a Condition) to a response (an Action). When apply is invoked, the reflex map iterates the current action, if any. If no action is currently selected, ReflexMap iterates its list of triggers. The first satisfied trigger condition causes it’s response to become current.

As you can see, the API is based on objectified functions. This is a choice, maybe I’ll get a chance to explain why another time.

An example

Here is sample code used to program simple behavior for a guarding dog:

Actor* dog=[[Actor alloc]initWithX:0 y:0 z:0 tag:@"dog"];
//
Condition* outer=[[OnApproach alloc]initWithSource:dog target:@"human" range:60.0f];
Condition* inner=[[OnApproach alloc]initWithSource:dog target:@"human" range:30.0f];
Condition* close=[[OnApproach alloc]initWithSource:dog target:@"human" range:10.0f];
//
XAction* bark=[[ActorGesture alloc]initWithActor:dog action:BARK];
XAction* bite=[[ActorGesture alloc]initWithActor:dog action:BITE];
XAction* chase=[[MoveTo alloc]initMovingActor:dog to:nil];
//
ReflexMap* reflexes=[[ReflexMap alloc]init];
[reflexes add:bite on:close];
[reflexes add:chase on:inner];
[reflexes add:bark on:outer];

The @”foo” bits just represent strings (in case you’re not too familiar with Objective C). I use those strings as tags, so the @”human” tag allows OnApproach to only target human intruders (not rabbits, leprechauns and so forth…)

As illustrated by this code, although the idea of defining rules using conditions and actions seems OK at first, the design isn’t. The problem is that a Condition (in this case OnApproach) can be fulfilled in various ways. When the condition is fulfilled, we need to pass data to the action to effect it correctly. So in this case onApproach is fulfilled with a given target, but MoveTo cannot bind the target detected by OnApproach. Let’s try to fix this.

Binding precondition output to postcondition input – 1: using arbitrary parameters

My first impression is that the OnApproach condition should be replaced by an event call processed by a listener, so we’d have something like:

on:(Actor*)actor approaches:(Actor*) agent{...}

The downside is that doing this will require that we re-implement the event handling function for every trigger/response pair. We don’t want to write a new class for that every time. We do need to bind the output of the trigger to the input of the matching action. We might then change change the Action and Condition classes:

@interface Condition
-(BOOL)check; // unchanged. we need to return a boolean value anyway.
-(NSArray*)result; // OnApproach will place a reference to the approaching actor in this array.
@end

@interface Action
-(void)apply:(NSArray*)param // allows injecting data into the triggered action.
@end

We still need to tell ReflexMap which parameters to inject into MoveTo. So we change a function in ReflexMap:

add:(Action*)action on:(Condition*)trigger;

Now becomes:

add:(Action*)action with:(int[])params on:(Condition*)trigger;

And our reflexes are then setup as follows:

int[] args={0}; // addresses the only output from OnApproach
[reflexes add:bite with:args on:close];
[reflexes add:chase with:args on:inner];
[reflexes add:bark with:args on:outer];

This solves the problem, but lacks clarity because conditions and actions now return/accept arrays of arbitrary length and content.

Binding precondition output to postcondition input – 2: sharing placeholders

The next idea is as follows: what if we could arrange that whatever target defined by OnApproach is addressable using MoveTo?
To do this requires modifying both OnApproach and MoveTo. On the bright side, we needn’t change the Action and Condition signatures:

  1. I defined an ActorRef class. ActorRef owns an Actor property (I could use a pointer to pointer, but that would be a little confusing).
  2. OnApproach defines targetRef as an ActorRef instance. When a target is detected, targetRef.actor is assigned.
  3. MoveTo also defines a targetRef property. When MoveTo is initialized, we pass the ActorRef instance owned by our OnApproach condition.

We can now change the original example as follows (skipping the bits that didn’t change):

OnApproach* inner=[[OnApproach alloc]initWithSource:dog target:@"actor" range:30.0f];
XAction* chase=[[MoveToActor alloc]initMovingActor:dog to:inner.target];

This seems to solve our problem nicely:

  • Type safety isn’t compromised.
  • The core interfaces remain simple
  • The binding process doesn’t involve ReflexMap
  • The code used to define our test script is simple and readable, compared to solution (1).

OK, enough for today then. Next time I’ll look into a way to further simplify behavioral scripts, as they are still a little heavy.