Re: Blocking for input during a loop?
Re: Blocking for input during a loop?
- Subject: Re: Blocking for input during a loop?
- From: Per Bull Holmen <email@hidden>
- Date: Tue, 19 Jan 2010 06:10:26 -0800 (PST)
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 (email@hidden)
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:
This email sent to email@hidden