tutorial 8

In this tutorial, I'll bring you through a particle engine. It reads from files in the standard C method of reading. Most of this is just like Nehe's tutorial on particle engines, except reading from files to make something a bit more flexible so we don't need to recompile to get a different effect.

When I upgraded this code to Jaguar, I unfortunately found that my jpgs wouldn't load. If you are having the same problem, fear not! There is some sort of problem with jpgs created with a certain version of Adobe Photoshop. Some of the code in previous tutorials became redundant when Jaguar came out, so the hacky kludges aren't really important anymore (for instance, the "first" workaround to get first responder status is no longer needed).

On with the tutorial!

First, some definitions. We're going to arbitrarily decide the maximum number of particles in an engine at compile time. It would have been just as easy to read this from a file too, but this way we avoid dynamic allocation of some huge array when some user of the program decides he'd like ten million particles.

Next, we declare the individual particle structure. Classes are expensive in Objective C, much more so than in C++, so we want to make sure we don't declare extra classes for every little thing. In our particle, we're going to keep track of it's location, color, velocity, size and time to live (ttl). In addition, we're going to allow each particle to change it's size and color, so we keep track of that as well.


#define MAXPARTICLES 1000 #define PI 3.14 typedef struct _particle { float location[3]; float color[4] ; float color_vector[4]; float velocity[3] ; float size; float size_vector; float ttl; } particles; enum flags { CREATE, UPDATE, UPDATE_AND_CREATE };

Here are the variables in the particle engine itself. Almost all off these can be set by the person making up the particle engine. Each particle engine can have one texture associated with it. In this particular iteration of the particle engine, any location changes must be made within the program itself.


@interface NSGLParticleEngine : NSObject { particles particle[MAXPARTICLES]; int numParticles; GLuint texture; float particlesPerSecond; float emissionResidue; float start_color[4]; float dest_color[4]; float color_variance[4]; float location[3]; float velocity[3]; float location_variance[4]; float gravity[3]; float start_size; float dest_size; float size_variance; float spin; float rotation; float usevel[3]; float angle; float ttl; double lastupdate; } ... @end

Finally we come to the methods. First, we have the initialization of the particle engine. This is actually just a token. The real work happens in readFile and initParticle. TickCount() is a nice little Apple function that returns the number of ticks since the system started up. This unfortunately could lead to errors if the count gets too high.


- (id) initWithFile: (NSString *)filename { int i; [self readFile: filename];
// need this or we'll end up with divide by zero errors
if( ttl == 0 ) ttl = 0.01; for( i=0; i < numParticles; i++ ) [self initParticle: i]; numParticles = MAXPARTICLES; lastupdate = TickCount(); return self; }

When I wrote this, I was having a few problems because I was writing it to be easily converted to straight C-code for a graphics assignment. Nehe's didn't do things quite like this, so this one is a bit... bigger...

Basically, what it's doing is


if( [s isEqualToString: @"variable_name"] ) scan parameters into variables and/or perform operations

Most of what you see in this method is just a whole bunch of different items being scanned like this. The method ignores unidentified lines and comments denoted with //. The formatting for this method isn't great, but hopefully you'll forgive me for that.


- (void) readFile: (NSString *) nsPath { const char *path = [nsPath cString]; NSString *s; FILE *file; unsigned char buffer[1024]; float f, c0, c1, c2, c3; size_t size; file = fopen( path, "r" ); if( file == nil ) { NSLog(@"File did not open"); } else { while( ! feof(file) ) { fscanf( file, "%s", buffer ); s = [NSString stringWithCString: buffer]; if( [s hasPrefix: @"//"] ) fgetln( file, &size ); else if( [s isEqualToString: @"ttl" ] ) { fscanf( file, "%s%f", buffer, &f ); ttl = f; NSLog(@"ttl = %f", ttl); } else if( [s isEqualToString: @"texture" ] ) { fscanf( file, "%s", buffer ); fscanf( file, "%s", buffer ); s = [NSString stringWithCString: buffer]; texture = getTextures( s ); NSLog(@"texture = %d", texture); NSLog( s ); } else if( [s isEqualToString: @"usevelocity" ] ) { fscanf( file, "%s%f%f%f", buffer, &f, &c0, &c1 ); usevel[0] = f; usevel[1] = c0; usevel[2] = c1; NSLog(@"usevel = %f %f %f", usevel[0], usevel[1], usevel[2]); } else if( [s isEqualToString: @"numparticles" ] ) //part_per_sec_change_function { fscanf( file, "%s%f", buffer, &f ); numParticles = f; NSLog(@"numP = %f", numParticles); } else if( [s isEqualToString: @"start_color" ] ) { fscanf( file, "%s%f%f%f%f", buffer, &c0, &c1, &c2, &c3); start_color[0] = c0; start_color[1] = c1; start_color[2] = c2; start_color[3] = c3; NSLog(@"color = %f %f %f %f", c0, c1, c2, c3); } else if( [s isEqualToString: @"dest_color" ] ) { fscanf( file, "%s%f%f%f%f", buffer, &c0, &c1, &c2, &c3); dest_color[0] = c0; dest_color[1] = c1; dest_color[2] = c2; dest_color[3] = c3; NSLog(@"dest color = %f %f %f %f", c0, c1, c2, c3); } else if( [s isEqualToString: @"color_var" ] ) { fscanf( file, "%s%f%f%f%f", buffer, &c0, &c1, &c2, &c3); color_variance[0] = c0; color_variance[1] = c1; color_variance[2] = c2; color_variance[3] = c3; NSLog(@"color var = %f %f %f %f", c0, c1, c2, c3);} else if( [s isEqualToString: @"location" ] ) { fscanf( file, "%s%f%f%f", buffer, &c0, &c1, &c2); location[0] = c0; location[1] = c1; location[2] = c2; NSLog(@"location = %f %f %f", c0, c1, c2); } else if( [s isEqualToString: @"location_var" ] ) { fscanf( file, "%s%f%f%f", buffer, &c0, &c1, &c2); location_variance[0] = c0; location_variance[1] = c1; location_variance[2] = c2; NSLog(@"location var = %f %f %f", c0, c1, c2); } else if( [s isEqualToString: @"velocity" ] ) { fscanf( file, "%s%f%f%f", buffer, &c0, &c1, &c2); velocity[0] = c0; velocity[1] = c1; velocity[2] = c2; NSLog(@"velocity = %f %f %f", c0, c1, c2); } else if( [s isEqualToString: @"gravity" ] ) { fscanf( file, "%s%f%f%f", buffer, &c0, &c1, &c2); gravity[0] = c0; gravity[1] = c1; gravity[2] = c2; NSLog(@"gravity = %f %f %f", c0, c1, c2); } else if( [s isEqualToString: @"part_per_sec" ] ) { fscanf( file, "%s%f", buffer, &c0 ); particlesPerSecond = c0; NSLog(@"pps = %f", c0); } else if( [s isEqualToString: @"start_size" ] ) { fscanf( file, "%s%f", buffer, &c0 ); start_size = c0; NSLog(@"size = %f", c0); } else if( [s isEqualToString: @"dest_size" ] ) { fscanf( file, "%s%f", buffer, &c0 ); dest_size = c0; NSLog(@"dsize = %f", c0); } else if( [s isEqualToString: @"size_var" ] ) { fscanf( file, "%s%f", buffer, &c0 ); size_variance = c0; NSLog(@"sizevar = %f", c0); } else if( [s isEqualToString: @"angle" ] ) { fscanf( file, "%s%f", buffer, &c0 ); angle = c0; NSLog(@"angle = %f", c0); } else if( [s isEqualToString: @"spin" ] ) { fscanf( file, "%s%f", buffer, &c0 ); spin = c0; NSLog(@"spin = %f", c0); } } } }

Next we move on to initParticle. For this method, we specify the particle number that should be initialized. In my code, I've taken considerably more random numbers in order to make everything really random. We set up a time to live that is approximately the same as the others (but each is slightly different). We also make the color slightly random (based on user parameters), as well as the initial location and velocity vector.


- (void) initParticle: (int)i { float random_number; float random_pitch, random_yaw;
// time to live = the average time to live * random number * 2 ( [-1,1]*ttl )
particle[i].ttl = ttl * frand() * 2;
// this means that we will likely have a shade of the initial color
random_number = frand()*2 - 1; particle[i].color[0] = clamp( start_color[0] + random_number*color_variance[0], 0, 1); particle[i].color[1] = clamp( start_color[1] + random_number*color_variance[1], 0, 1); particle[i].color[2] = clamp( start_color[2] + random_number*color_variance[2], 0, 1); particle[i].color[3] = clamp( start_color[3] + random_number*color_variance[3], 0, 1);
// this means that we will likely have a shade of the final color
random_number = frand()*2 - 1; particle[i].color_vector[0] = clamp( dest_color[0] + random_number*color_variance[0] - particle[i].color[0], -1, 1)/particle[i].ttl; particle[i].color_vector[1] = clamp( dest_color[1] + random_number*color_variance[1] - particle[i].color[1], -1, 1)/particle[i].ttl; particle[i].color_vector[2] = clamp( dest_color[2] + random_number*color_variance[2] - particle[i].color[2], -1, 1)/particle[i].ttl; particle[i].color_vector[3] = clamp( dest_color[3] + random_number*color_variance[3] - particle[i].color[3], -1, 1)/particle[i].ttl;
// this means that we will start around the area of the particle engine
random_number = frand()*2 - 1; particle[i].location[0] = location[0] + random_number*location_variance[0]; particle[i].location[1] = location[1] + random_number*location_variance[1]; particle[i].location[2] = location[2] + random_number*location_variance[2]; random_number = frand()*2 - 1; random_yaw = frand() * PI * 2.0; random_pitch = frand() * angle * PI / 180.0; particle[i].velocity[0] = velocity[0]*usevel[0] + velocity[0]*cos(rotation*PI/180.0) * cos(random_pitch); particle[i].velocity[1] = velocity[1]*usevel[1] + velocity[1]*sin(rotation*PI/180.0) + velocity[1] * sin(random_pitch) * cos(random_yaw); particle[i].velocity[2] = velocity[2]*usevel[2] + velocity[2] * sin(random_pitch) * sin(random_yaw); random_number = frand()*2 - 1; particle[i].size = start_size; particle[i].size_vector = dest_size + random_number*size_variance - particle[i].size; }



     Download the project builder files

 

return to deep cocoa / cocoagl tutorials