Table of content

Previous: 6.a The cylindrical coordinate system

# b. The Soul Reaver portal effect

To illustrate this lesson, I'm including my own rendition of the cool portals in the game Soul Reaver: Legacy of Kain (contained in lesson6.wed). As you might remember, they are made of concentric, horizontal circles on the ground which expand before fading out. In the middle, when the player is far, white sparks fly straight up randomly. Once the player comes close enough, the sparks change their pattern of flight into a sort of spiral or string which circles the portal while rising (see Figure 6.5). Cool and original effect I must say (like most of this amazing game). As shown below, we can reproduce this effect with very little effort, which shows that professional game developers already know everything about cylindrical and polar coordinates.

There are 2 actions and one function in the level. Action a_portal is attached to an invisible entity. It create the concentric circles which expand and fade out using ordinary sprite entities representing circles, with action a_circle attached to them. It also emits particles to which we attach function spark_rise().

Let us start with the action of the circles:

    action a_circle
    {
    	my.passable = on;
    	my.oriented = on;
    	my.flare = on;
    	my.bright = on;
    	my.blue = 255;
    	my.green = 255;
    	my.red = 255;
    	my.alpha = 100;
    	my.tilt = 90;
    	my.scale_x = 0.1;
    	my.scale_y = 0.1;
    	my.light = on;
    	my.ambient = -255;
    	my.unlit = on;

    	while(my.scale_x<2)
    	{
    		// expand the circle
    		my.scale_x += 0.05*time;
    		my.scale_y = my.scale_x;

    		// dim the circle once scale>0.5
    		if (my.scale_x>0.5)
    		{
    			my.alpha = 100*(2-my.scale_x)/1.9;
    		}

    		wait(1);
    	}
    	remove(me);
    }

It is simple and does not have anything to do with cylindrical coordinates. :-) Circles are created at the position of the portal entity, and oriented parallel to the ground. They then steadily expand until their scale becomes large than 2 when they are removed from the level. Their opacity also declines from 100 to 0 throughout their life span.

The function of the particles also does not have anything special:

    bmap spark_image = <novaw1.pcx>;

    function spark_rise()
    {
    	if (my.lifespan==80)
    	{
    		my.move = on;
    		my.bmap = spark_image;
    		my.size = 25;
    		my.flare = on;
    		my.bright = on;
    		my.alpha = 100;
    		my.red = 255;
    		my.green = 255;
    		my.blue = 255;
    		my.gravity = -1;
    		my.lifespan = 16;
    	}

    	my.size -= 1*time;
    	my.alpha -= 5*time;
    }

After being initialized, they just become smaller and more transparent as they grow older. We also give them negative gravity so that they linearly accelerate toward the ceiling.

Ok, so nothing particular goes on in the functions of the circles and the sparks. Then, where is the complex stuff which turns random-looking particles into a spiral? All of it is in the function of the portal:

    var delay_circles = 16;
    var delay_sparks = 0.5;

    var radius_disk = 15;
    var ang_velocity = 15;

    string circle_image = <circle.pcx>;

    action a_portal
    {
    	wait(2);
    	fog_color = 1;
    	camera.fog = 40;

    	my.invisible = on;
    	my.passable = on;

    	my.skill1 = delay_circles;
    	my.skill2 = delay_sparks;

    	// to compute distance camera-portal
    	var temporary;
    	var distance;

    	// to create sparks
    	var r;
    	var phi;
    	var position;
    	var velocity;
    	var spark_speed;

    	while(1)
    	{
    		// computes distance between camera and portal
    		vec_set(temporary.x,my.x);
    		vec_diff(temporary.x,temporary.x,camera.x);
    		distance = vec_length(temporary.x);

    		// create a circle
    		if (my.skill1==delay_circles)
    		{
    			ent_create(circle_image,my.x,a_circle);
    		}

    		// create sparks that rise if camera farther than 500 quants
    		if ((my.skill2==delay_sparks) && (distance>500))
    		{
    			// we pick a location RANDOMLY in the circle r<radius
    			r = random(radius_disk);
    			phi = random(360);
    			position.x = r*cos(phi);
    			position.y = r*sin(phi);
    			position.z = my.z+10;
    			// we give a slight radial speed
    			spark_speed = random(2.5);
    			velocity.x = spark_speed*cos(phi);
    			velocity.y = spark_speed*sin(phi);
    			velocity.z = 0;
    			// create the particle
    			effect(spark_rise,1,position.x,velocity.x);
    		}

    		// creates a string of sparks that circle the portal
    		if ((my.skill2==delay_sparks) && (distance<500))
    		{
    			// we pick a ROTATING location on the circle r = 2*radius_disk
    			phi = ang_velocity*total_ticks;
    			position.x = 2*radius_disk*cos(phi);
    			position.y = 2*radius_disk*sin(phi);
    			position.z = my.z+10;
    			// create the particle
    			effect(spark_rise,1,position.x,nullvector);
    		}

    		// update the timer for the circles
    		if (my.skill1>0)
    		{
    			my.skill1 -= time;
    		}
    		else
    		{
    			my.skill1 = delay_circles;
    		}

    		// update the timer for the sparks
    		if (my.skill2>0)
    		{
    			my.skill2 -= time;
    		}
    		else
    		{
    			my.skill2 = delay_sparks;
    		}

    		wait(1);
    	}
    }

It is a little long but not complicated compared to what we saw in the previous lessons. We see that variables skill1 and skill2, and a few lines of code are used to implement delays between the creation of successive circles and sparks (delay_circles = 16 ticks for circles and delay_sparks = 0.5 ticks for sparks).

Now let us focus on the instructions which create the sparks (circles are produced in completely standard fashion). Since we want the aspect of the portal to change according to the distance between the portal and the player, we need to compute the distance between them at each frame. This is done with simple vector algebra (see equations (1.5) and (1.6)).

Vector temporary is first defined as pointing from the camera entity to the portal entity like so:

    			temporary  = portal.x - camera.x

We then get the distance between the portal and camera by applying the function vec_length() (see equation (1.7)) to it. The resulting value is stored in the variable distance:

    		vec_set(temporary.x,my.x);
    		vec_diff(temporary.x,temporary.x,camera.x);
    		distance = vec_length(temporary.x);

Figure 6.5: (left) Portal when the camera is far: the sparks rise randomly in the middle of the portal. (right) When the camera is close, the sparks now form a string which circles over the portal.

Now for the particle effects themselves. What we need is to have sparks which look like bubbles over a glass of champagne if distance is larger than, say, 500 quants. If distance<500 then the sparks should form a spiral over the portal.

First the easy part: making sparks bubble over the portal. By analogy with a glass of champagne, we will create sparks at random locations dawn inside a disk of radius radius_disk = 15 quants and at a height 10 quants over the portal. To do this, we use Figure 6.3 (right): every 0.5 ticks, we draw points with coordinates (r,phi,my.z+10) with r between 0 and radius_disk and phi between 0 and 360 degrees, and create a spark there (my refers to the portal entity in action a_portal). To add further randomness to the effect, we give the sparks a small radial velocity vector velocity pointing outwards from the portal (using the same equations as for the missile in lesson 5).

    			// we pick a location RANDOMLY in the circle r<radius
    			r = random(radius_disk);
    			phi = random(360);
    			position.x = r*cos(phi);
    			position.y = r*sin(phi);
    			position.z = my.z+10;
    			// we give a slight radial speed
    			spark_speed = random(2.5);
    			velocity.x = spark_speed*cos(phi);
    			velocity.y = spark_speed*sin(phi);
    			velocity.z = 0;
    			// create the particle
    			effect(spark_rise,1,position.x,velocity.x);

The overall result is indeed a cloud of sparks which bubble above the portal with a nice circular symmetry.

Now for the spiral. After a little thinking and observing the effect on my old Playstation console, I realized that all we need to do is to create particles at a point, a 'source', which moves with constant velocity around a horizontal circle. To see how we could do this, let us check out Figure 6.2 (middle): a point which traces out a horizontal circle is represented by the coordinates (r,phi,my.z+10) with r and z constant and phi going from 0 to 360 degrees.

    			// we pick a ROTATING location on the circle r = 2*radius_disk
    			phi = ang_velocity*total_ticks;
    			position.x = 2*radius_disk*cos(phi);
    			position.y = 2*radius_disk*sin(phi);
    			position.z = my.z+10;
    			// create the particle
    			effect(spark_rise,1,position.x,nullvector);

We choose r = 2*radius_disk, z = my.z + 10 as before, and phi = ang_velocity*total_ticks: the source circles the portal with an angular velocity ang_velocity = 15 degrees/tick. These exact values were fine-tuned so that the 'source' of the particle moved slowly enough for the sparks to rise but at the same time to remain close together and look like a string. That's it! This gives the desired effect (check it out if you do not believe me ;-)).

Note that we could have done something more complicated using a leader which circles around the portal, and the equations from lesson 4 to leave a trail. Some other time maybe. ;-)

Well, that is it for the Soul Reaver portal and cylindrical coordinates. One last mathematical lesson: spherical coordinates. Take courage, this one is a little harder than the others. See you there!

Next: Lesson 7. Spherical coordinates: creating spheres out of particles