Showing posts with label Events. Show all posts
Showing posts with label Events. Show all posts

Saturday, 8 August 2009

Implementing Transient Events - an update

After some feedback, I've made a few tweaks to the TGameTimer class. Here is the new Tick method:
{ Decrease the duration of the timer by a turn }
function TGameTimer.Tick: Boolean;
begin
{ Trigger events if they are defined }
if (FTimerDurationLeft > 0) then
begin
{ Interval Event }
if (Assigned(FTimerTickEvent)) then
FTimerTickEvent(Self);
end
else if (FTimerDurationLeft = 0) then
begin
{ End Event }
if (Assigned(FTimerEndEvent)) then
FTimerEndEvent(Self);
end;

{ Decrement the Counter }
Dec(FTimerDurationLeft);

{ Return true if the Timer is still active }
Result := GetStatus;
end;
Moving the decrement of the tick timer to after the tick events means that it now correctly works with a duration of one turn - in these circumstances previously only the Start and End Events would be fired.

The interface to the Event Hooks themselves have changed

{ Procedure Pointer for Event Hook }
type TGameTimerEvent = procedure(const GameEvent: TObject);

This allows passing in of the triggering TGameTimer object (referenced by Self in the tick method, and subsequently cast to TGameTimer in the actual hook).

For those of you familiar with Delphi this is pretty much (with a few minor differences) what TNotifyEvent does.

I've also removed the Progress property entirely. Since the Tick event is designed to be called every game turn then its simply a matter of comparing FTimerDuration and FTimerDurationLeft as needed there instead.

Thursday, 6 August 2009

Implementing Transient Events: An example

This is a video of a simple usage of the TGameTimer class - to increase and decrease the FOV of the Player. This takes place using an event of 30 turns duration.

The default radius of the FOV for this demo is 3 squares (normally it is 7), and at the start of the event, this is increased to 9. Half-way through (15 turns later), this is reduced to 6, and then 15 turns after that, the radius is reduced to 3. Appropriate messages are displayed in the messagelog.

I'll go through the code required to produce this in my next post.



It is worthwhile to note that this means that I can now implement variable stage poisons and sickness, like Crawl posseses. But I have many ideas for Kharne beyond even what Crawl implements.

Implementing Transient Events: The TGameTimer class

One of the most difficult tasks in programming a roguelike is to handle transient events, such as poison, strength buffs and so on. As well as the effects (lose a HP! gain strength!), you've got to keep track of the duration, and deal with cancelling the events. One (obviously unsatiisfactory) way of implementing this is to hard code it The other, is to use a class to handle such events.

Hence the TGameTimer class.

I've made the full source code available here and here, and also listed it below (you may need to copy and past to get around wordwrapping issues). Although written in Delphi, it should compile in FreePascal without too many changes (if any). I'm releasing it as public domain, so feel free to use and abuse it as you see fit.

Basically, the class uses good ole' Procedure Pointers to allow (optional) event hooks to occur in three scenarios:

  • Upon Event Creation
  • Upon an Event Tick
  • Upon Event Expiry

The constructor of the object takes an event type and an event duration in turns. as well as the event hooks themselves.

If anyone wants a demo app showing the class in action, let me know and I'll post one. Otherwise, you'll have to wait until the next major release of Kharne, when I intend to use TGameTimer to implement potion functionaility (finally!).

I'll do a followup post over the weekend with some additional comments, and thoughts on how it can be improved. In the meantime, comments are most welcome.


{ Timer Handling

A TGameTimer represents a transient event that has a duration.

An example would be the side-effects of drinking a Potion of Might - it increases
strength for a limited number of turns. We optionally define a number of procedure
pointers to point to events that occur at the beginning and end of the duration,
and also on every turn.

For example, upon drinking a Potion of Might, the event might display a message
stating you feel mighty (as well as increasing your strength). As the duration
of the effects decrease, further messages will be displayed stating that the
effects of the potion are wearing off, and then when the duration has expired,
your strength reverts back to normal

To implement this, use the follwing steps as a guide:

1. Set up a variable:

DrinkMightPotionEvent: TGameTimer;

2. Define three events as follows:

procedure MightPotionDrink(const Turns: Integer = 0);
procedure MightPotionTick(const Turns: Integer = 0);
procedure MightPotionEnd(const Turns: Integer = 0);

procedure MightPotionDrink(const Turns: Integer);
begin
DisplayMessage('You feel mighty!');
Player.Strength := Player.Strength + 10;
end;

procedure MightPotionTick(const Turns: Integer);
begin
if (DrinkMightPotionEvent.Progress = 50) then
DisplayMessage('The effects are wearing off!');
Player.Strength := Player.Strength - 5;
end;

procedure MightPotionEnd(const Turns: Integer);
begin
DisplayMessage('You no longer feel so mighty!');
Player.Strength := Player.Strength - 5;
end;

3. Instantiate the event:

DrinkMightPotionEvent := TGameTimer.Create(timMight,
DURATION_MIGHT_POTION,
MightPotionTick,
MightPotionDrink,
MightPotionEnd);

4. Then, on every turn that passes, simply call

if (Assigned(DrinkMightPotionEvent)) then DrinkMightPotionEvent.Tick;

}

{ Define the types of timers as an enum for simplicity }
type TGameTimerType = (timSpeed,
timConfusion,
timBlindness,
timSeeInvisible,
timParalysis,
timFreeAction,
timCombatMastery,
timReflexes,
timMight);

{ Procedure Pointer for Event Hook }
type TGameTimerEvent = procedure(const Turns: Integer = 0);

{ Class Definition - it inherits the ICommonObject interface to gain access
to the StringValue method to allow easy persistance }
type TGameTimer = class(ICommonObject)
private
FTimerType: TGameTimerType; // Timer Type, from the enum previously defined
FTimerDuration: Integer; // Starting Duration, in turns
FTimerDurationLeft: Integer; // Duration Left, in turns
FTimerTickEvent: TGameTimerEvent; // Optional Event to call on each decrement
FTimerStartEvent: TGameTimerEvent; // Optional Event to call on starting the timer
FTimerEndEvent: TGameTimerEvent; // Optional Event to call on ending the timer (i.e. turns left = 0)

{ Private functions to support class properties defined below }
function GetStatus: Boolean;
function GetProgress: Integer;

public
{ Standard Constructor }
constructor Create(TimerType: TGameTimerType;
TimerDuration: Integer;
TimerTickEvent: TGameTimerEvent = nil;
TimerStartEvent: TGameTimerEvent = nil;
TimerEndEvent: TGameTimerEvent = nil);

{ Decrement the Timer by one turn. Will return true if the timer has not expired }
function Tick: Boolean;

{ Interface Method for Persistance }
function GetStringValue: String;

{ Properties }
property TimerType: TGameTimerType read FTimerType; // Return the enum
property TimerDuration: Integer read FTimerDurationLeft; // Number of Turns left
property TimerProgress: Integer read GetProgress; // Number of Turns left as a percantage (0-100) of the original duration
property Active: Boolean read GetStatus; // True if Number of Turns left is > 0
end;


implementation

{ Standard Constructor - this is deliberately the only way to set up the duration etc }
constructor TGameTimer.Create(TimerType: TGameTimerType;
TimerDuration: Integer;
TimerTickEvent: TGameTimerEvent;
TimerStartEvent: TGameTimerEvent;
TimerEndEvent: TGameTimerEvent);
begin
{ Load the private member data }
FTimerType := TimerType;
FTimerDuration := TimerDuration;
FTimerDurationLeft := TimerDuration;
FTimerTickEvent := TimerTickEvent;
FTimerStartEvent := TimerStartEvent;
FTimerEndEvent := TimerEndEvent;

{ If we have a start event defined then execute it now }
if (Assigned(FTimerStartEvent)) then
FTimerStartEvent(FTimerDurationLeft);
end;

{ Return true if the timer hasn't expired }
function TGameTimer.GetStatus: Boolean;
begin
Result := FTimerDurationLeft > 0;
end;

{ Decrease the duration of the timer by a turn }
function TGameTimer.Tick: Boolean;
begin
Dec(FTimerDurationLeft);

{ Trigger events if they are defined }
if (FTimerDurationLeft > 0) then
begin
{ Interval Event }
if (Assigned(FTimerTickEvent)) then
FTimerTickEvent(FTimerDurationLeft);
end
else if (FTimerDurationLeft = 0) then
begin
{ End Event }
if (Assigned(FTimerEndEvent)) then
FTimerEndEvent(FTimerDurationLeft);
end;

{ Return true if the Timer is still active }
Result := GetStatus;
end;

{ Returns the number of Turns left as a percantage (0-100) of the original duration }
function TGameTimer.GetProgress: Integer;
var
Percentage: Double;
begin
Percentage := FTimerDurationLeft / FTimerDuration;
Result := Trunc(Percentage * 100);
end;

{ Get the Timer as a String }
function TGameTimer.GetStringValue: String;
begin
{ TODO: We will need to extend this to allow hashing of the attached procedures }
Result := Format('%d%d', [Ord(FTimerType), FTimerDuration]);
end;