Ben Haller wrote:
> On 18-Jan-10, at 9:29 PM, Per Bull Holmen wrote:
> 
> > ...Again, I have clearly not explained what I wanted to do properly. No, I 
> > did not want what you describe. I have done it now, I chose to use threads, 
> > and it got far cleaner than the "standard Cocoa way". No, I don't have to 
> > handle any other input than what's relevant for the game, Cocoa handles the 
> > rest, thank you... :)
> 
>   Perhaps you could post your solution, for the edification of the list?

Yes. It's very unfinished, and likely to contain bugs and memory leaks, though. 
I haven't even started looking at memory handling.

> > I really appreciate your replies! I must have explained it poorly... anyway 
> > if someone still want to convince me I'm doing things the wrong way, then 
> > I'm guessing either they are so in love with MVC they have lost their sense 
> > of reality, or they still haven't understood what I'm trying to explain... 
> > :)
> 
>   Or perhaps, once you post your solution, someone will point out a problem 
> with it that you haven't foreseen?

Definitely. I'm not trying to say my solution is guaranteed to be the best! 
Just that I don't agree MVC is always the best way by definition. It does still 
depend on what you're trying to do, and it is also a matter of taste, feel free 
to dislike my solution! I should explain to you that this game is just for 
myself and friends, though, not for commercial release.

/* ------------------------------------------------------------

I have made one class called GameController, which is overridden by subclasses 
to control one specific type of game (PS: I will soon rename the class 
"Controller" to "MainController", and instance variable "controller" to 
"mainController"). The GameController base class also mediates between input 
from the main thread, and the game thread, with help from the class InputBuffer:

----------------------------------------------------------------- */

/* GameController */

#import <Cocoa/Cocoa.h>
#import "TonePlayer.h"
#import "Controller.h"

typedef enum {
NoInput = 0,
CodeInput,
ToneInput
} InputType; // + response?! Senere!

typedef enum {
NoInputAvailable,
InputIsAvailable
} InputStatus;

@interface InputBuffer : NSObject {
int toneCodes[ 16 ];
int octaves[ 16 ];
InputType types[ 16 ];
NSConditionLock *inputLock;
int size;
}

-(void)addInput:(int)code; // Always called from main thread
-(void)addInputTone:(int)tone octave:(int)octave; // Always called from main 
thread
-(InputType)getInputToneCode:(int *)toneCode octave:(int *)octave; // Always 
called from game thread
-(BOOL)isEmpty;

@end

@interface GameController : NSObject {
TonePlayer *tonePlayer;
InputBuffer *inputBuffer;
IBOutlet id controller;
}

// public
-(void)runGame; // Launches game loop in background...
-(void)acceptInputTone:(int)tone octave:(int)octave; // Accepts input when game 
loop is running...
-(void)acceptInput:(int)code;

// private
-(int)hiOctave;
-(int)loOctave;
-(int)randomOctave;
-(int)waitForInput;
-(void)waitForInputTone:(int *)tone octave:(int *)octave;
-(int)waitForInputTone:(int *)tone octave:(int *)octave code:(int *)code;
-(int)askRetries;
-(void)playTone:(int)tone octave:(int)octave;
-(Controller *)controller;
-(void)backGroundGameLoop:(id)dummy; // Don't override. Launched in new thread 
for game loop.

-(void)prepareGameLoop;              // Overriden by subclasses to prepare for 
the game, in the main thread...
-(void)doGameLoop;                   // Overriden by subclasses to do the 
actual work, in a separate thread...
-(void)cleanupGameLoop:(id)dummy;    // Overriden by subclasses to clean up 
after the game, in the main thread...

@end

/* ----------------------------------------------------------------

Notice that "game loop" here does not mean the repetitive, periodic game loop 
which is common for action games. This is not an action game, but one that 
waits patiently for the user to respond to questions. The loop is a loop of 
questions, but each subclass is free to define a radically different game flow. 
OK, here is the implementation of the input buffer. This is probably 
overkill... :)

---------------------------------------------------------------- */

@implementation InputBuffer

-(id)init {
inputLock = [[NSConditionLock alloc] initWithCondition:NoInputAvailable];
return( self );
}

-(void)purge { // Don't call if empty!!
if( --size ) {
memmove( toneCodes, &toneCodes[ 1 ], size * sizeof( int ) );
memmove( octaves, &octaves[ 1 ], size * sizeof( int ) );
memmove( types, &types[ 1 ], size * sizeof( InputType ) );
}
}

-(void)addInput:(int)code { // Always called from MAIN thread
[inputLock lock];
if( size == 16 )
[self purge];
toneCodes[ size ] = code;
types[ size ] = CodeInput;
++size;
[inputLock unlockWithCondition:InputIsAvailable];
}

-(void)addInputTone:(int)tone octave:(int)octave { // Always called from MAIN 
thread
[inputLock lock];
if( size == 16 )
[self purge];
toneCodes[ size ] = tone;
octaves[ size ] = octave;
types[ size ] = ToneInput;
++size;
[inputLock unlockWithCondition:InputIsAvailable];
}

-(InputType)getInputToneCode:(int *)tone octave:(int *)octave { // Always 
called from GAME thread
[inputLock lockWhenCondition:InputIsAvailable];
InputType type = types[ 0 ];
*tone = toneCodes[ 0 ];
if( octave )
*octave = octaves[ 0 ];
[self purge];
[inputLock unlockWithCondition:( size ? InputIsAvailable : NoInputAvailable )];
return( type );
}

-(BOOL)isEmpty {
return( size ? YES : NO );
}

@end

/* --------------------------------------------------------------------------

Here is exerpts of the game controller base class:

-------------------------------------------------------------------------- */

@implementation GameController

-(id)init {
tonePlayer = [[TonePlayer alloc] init];
inputBuffer = [[InputBuffer alloc] init];
return( self );
}

// public
-(void)runGame { // Start game loop in background...
[self prepareGameLoop];
[NSThread detachNewThreadSelector:@selector( backGroundGameLoop: ) 
toTarget:self withObject:nil];
}
 
// The next two are called by the MainController class, when a button is 
pushed, or a game related action is initiated from a custom view, always from 
the main thread:
-(void)acceptInputTone:(int)tone octave:(int)octave { // Accepts input when 
game loop is running...
[inputBuffer addInputTone:tone octave:octave];
}

-(void)acceptInput:(int)code {
[inputBuffer addInput:code];
}

// private

// The next three are called by subclasses, always from the game thread:
-(int)waitForInput {
int code;
InputType type;
do { type = [inputBuffer getInputToneCode:&code octave:nil]; } while( type != 
CodeInput );
return( code );
}

-(void)waitForInputTone:(int *)tone octave:(int *)octave {
InputType type;
do { type = [inputBuffer getInputToneCode:tone octave:octave]; } while( type != 
ToneInput );
}

-(int)waitForInputTone:(int *)tone octave:(int *)octave code:(int *)code {
int toneCode;
InputType type = [inputBuffer getInputToneCode:&toneCode octave:octave];
switch( type ) {
case ToneInput:
*tone = toneCode;
break;
case CodeInput:
*code = toneCode;
break;
}
return( type );
}

// SNIP ...... //

-(void)backGroundGameLoop:(id)dummy {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self doGameLoop];
[self performSelectorOnMainThread:@selector( cleanupGameLoop: ) withObject:nil 
waitUntilDone:YES];
[pool release];
}

-(void)prepareGameLoop {
}

-(void)doGameLoop {
}

-(void)cleanupGameLoop:(id)dummy { // I'll have to find out later whether I 
really need those dummy arguments.
}

-(int)askRetries {
[controller askNumberLabel:@"Select how many retries you want for the game" 
initial:1 min:0 max:11];
return( [self waitForInput] );
}

@end

/* ------------------------------------------------------------------------

This might look overly complex, but the reason I'm doing it, is that writing 
the main game flow now becomes very easy. I'm planning to implement a large 
number of game types the user can select (all testing recognition of tones in 
different ways), and be able to change the logic/flow in a whim. It should not 
be limited to a set of questions 1-20, though the next implementation is. Here 
is the implementation of a subclass. The name WhichGameController is awful and 
might be changed, it just means the user hears a tone, and guesses "which" tone 
he hears. The user can enter tones via clicking on a guitar neck, or a "tone 
wheel" in a different window. Remember that "controller" will be renamed to 
"mainController".

--------------------------------------------------------------------------- */

@implementation WhichGameController

-(void)prepareGameLoop { // Main thread
// Setup UI
[[controller guitarNeck] orderFront:nil];
[[controller toneWheelWindow] orderFront:nil];
}

-(void)doGameLoop { // Secondary thread
NSUInteger allowedNotes[ 12 ], allowedCount;

// Get number of retries
int currentTone = -1, currentOctave = -1, newTone, newOctave, correctCount = 0;
int retries = [self askRetries];
// Let user choose what notes are allowed...
[[controller toneWheelView] setUserCanChangeAllowedNotes:YES];
[controller displayMessage:@"Select the allowed tones on the tonewheel, then 
press OK to start!", @"OK", @"Cancel", nil];
if( [self waitForInput] == 1 )
return;

do {
allowedCount = [[controller toneWheelView] getAllowedNotes:allowedNotes];

if( ! allowedCount ) {
[controller displayMessage:@"Select allowed tones!", @"OK", @"Cancel", nil];
if( [self waitForInput] == 1 )
return;
}
} while( ! allowedCount );
[[controller toneWheelView] setUserCanChangeAllowedNotes:NO];
// Start the game...
for( int i = 0; i < 20; i++ ) {
int retriesLeft = ( retries + 1 );
int guessTone;

[controller setFeedbackStatus:Neutral];

do {
newTone = allowedNotes[ [RandomGenerator randomIntMin:0 max:allowedCount] ];
newOctave = [self randomOctave];
} while( ( newTone == currentTone ) && ( newOctave == currentOctave ) );

currentTone = newTone;
currentOctave = newOctave;
BOOL correct = NO;
while( retriesLeft ) {
int action;
[self playTone:currentTone octave:currentOctave];
[controller displayMessage:@"Which tone is playing?", @"Replay", nil];
if( [self waitForInputTone:&guessTone octave:nil code:&action] == ToneInput ) {
if( correct = ( guessTone == currentTone ) ) {
correctCount++;
break;
}
[controller reportStatus:Wrong correctCount:correctCount totalCount:( i + 1 )];
retriesLeft--;
if( retriesLeft ) {
[controller displayMessage:@"Sorry, that was wrong!", @"Play your guess", @"Try 
again", nil];
while( [self waitForInput] == 0 )
[self playTone:guessTone octave:currentOctave];
}
}
}
[controller reportStatus:( correct ? Correct : Wrong ) 
correctCount:correctCount totalCount:( i + 1 )];
if( correct ) {
[controller displayMessage:[NSString stringWithFormat:@"Yup, that's correct! 
The correct tone was %@", GetToneName( currentTone )], @"Play again", @"Next 
tone", nil];
while( [self waitForInput] == 0 )
[self playTone:currentTone octave:currentOctave];
}
else {
[controller displayMessage:[NSString stringWithFormat:@"Sorry, you missed that 
one! The correct tone was %@", GetToneName( currentTone )], @"Play again", 
@"play your choice", @"Next tone", nil];
int choice;
while( ( choice = [self waitForInput] ) != 2 )
[self playTone:( ( choice == 0 ) ? currentTone : guessTone ) 
octave:currentOctave];
}
}
[controller displayMessage:[NSString stringWithFormat:@"You got %d out of 20 
right", correctCount], @"OK", nil];
[self waitForInput];

}

-(void)cleanupGameLoop:(id)dummy { // Main thread
// Clean up UI
[[controller toneWheelWindow] orderOut:nil];
}

@end

/* -----------------------------------------------------------------

Many statements here tells the main controller to update the UI, such as 
[controller displayMessage:@"Message", @"OK", nil];

This can't be done in a secondary thread, so the main controller encapsulates 
the info in objects, and sends the message to the main thread, like this:

-------------------------------------------------------------------- */

-(void)askNumberLabel:(NSString *)label initial:(int)initial min:(int)min 
max:(int)max { // This one is called by the game controller...
[self performSelectorOnMainThread:@selector( mainThreadAskNumber: ) 
   withObject:[NumberRequest label:label initial:initial min:min max:max]
waitUntilDone:NO];
}

-(void)mainThreadAskNumber:(id)numberRequest { // This one is private
NumberRequest *request = (NumberRequest *)numberRequest;
[numberStepper setIntValue:[request initial]];
[numberStepper setMinValue:[request min]];
[numberStepper setMaxValue:[request max]];
[numberTextField setIntValue:[request initial]];
[numberChooserLabel setStringValue:[request label]];
[tutorView setMainView:numberChooserView];
}

/* -------------------------------------------------------------------

I believe a possible bug is that, in rare cases, the game might respond to a 
user initiated action that actually came from a previous screen/request (if the 
user clicks twice fast). I haven't tried to fool it yet. The consequences would 
be minimal, but if it becomes a problem, I have a simple solution in mind.

---------------------------------------------------------------------- */
_______________________________________________

Cocoa-dev mailing list (Cocoa-dev@lists.apple.com)

Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com

Help/Unsubscribe/Update your Subscription:
http://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to arch...@mail-archive.com

Reply via email to