Projectile enemies are enemies that toss objects, such as arrows and fireballs, at the player if the player comes close enough. For the projectile to hit the player, there is a base from which the projectile will be generated. Depending on the distance from the player, the base will launch the projectile at a calculated angle and speed so that the projectile can reach the player. Sometimes, to increase the chance of hitting the player, more than one projectile is shot. In this example, we will shoot just one projectile, but we will make sure that we give the player a tough time.
I included the assets for the base and the rocket in the resource folder, so make sure that they are included in the project.
We will create two classes. The first one will be the shooter base, which will be responsible for checking the distance between the player and base. If the distance is less than a certain amount, then the projectile will be launched at the player. The projectile will be a separate class, which will handle the behavior of the projectile.
First, let's create the ShooterBase
class. Create a new file called ShooterBase
, and in the interface file, add the following:
#import "CCSprite.h" @interface ShooterBase : CCSprite{ CCSprite* _hero; CGPoint startPos; int xDirection; float xSpeed; //float xAmplitude; int shootCounter; } -(id)initWithFilename:(NSString *) filename Hero:(CCSprite*) hero pos: (CGPoint) position Direction: (int)Direction; @end
This is exactly same as the PatrolAI
class implementation. However, once again, we won't include the amplitude
variable here as it is not required.
Next, let's take a look at the implementation file, which is as follows:
#import "ShooterBase.h" #import "ProjectileAI.h" @implementation ShooterBase -(id)initWithFilename:(NSString *) filename Hero:(CCSprite*) hero pos: (CGPoint) position Direction: (int)Direction { if (self = [super initWithImageNamed:filename]) { _hero = hero; xDirection = Direction; xSpeed = 10.0f; shootCounter = 0; [self setPosition:position]; startPos = self.position; } return self; }
In the init
function, we will set the initial values as usual by executing the following code:
- (void)update:(CCTime)delta { shootCounter -- ; if(shootCounter <= 0) shootCounter = 0; float dist = ccpDistance(self.position, _hero.position); if(dist < 300){ if(shootCounter == 0){ shootCounter = 30; [self shoot]; } } if(self.position.x - _hero.position.x < 0) self.scaleX = -1.0; else if(self.position.x - _hero.position.x > 0) self.scaleX = 1.0; }
As in the case of PatrolAI
, we will decrement the counter. We will check whether it is less than zero; if it is, then we will set its value to zero.
As the shooter base is stationary, we won't change the position of the base. However, if you want the shooter base to move, you can add this functionality to it as we did with the PatrolAI
class.
We will then get the distance between the player and the shooter base. If it is less than 300
then we call the shoot
function and set the counter to 30
.
The shooter base is also scaled in the X
direction so that it faces the player while shooting:
-(void)shoot{ ProjectileAI* projectileRocket = [[ProjectileAI alloc]initWithFilename:@"ProjectileRocket.png" Hero:_hero pos:self.position Direction:self.scaleX * -1.0]; [[self parent]addChild:projectileRocket]; } @end
In the shoot
function, we will create an instance of ProjectileAI
and pass in the image, the reference to the hero
object, the position from which you want the projectile to be launched, and the direction in which the projectile needs to be launched.
Finally, we will add the projectile to the parent of the current class.
That is all for the ShooterBase
class! Let's add the ProjectileAI
class next.
First, let's take a look at the interface file. Add the following to the file:
#import "CCSprite.h" @interface ProjectileAI : CCSprite{ CCSprite* _hero; CGPoint startPos; int xDirection; float speed; bool isAlive; CGPoint velocity; CGPoint jumpForce; CGPoint gravity; CGPoint drag; } -(id)initWithFilename:(NSString *) filename Hero:(CCSprite*) hero pos: (CGPoint) position Direction: (int)Direction; @end
You will immediately see that apart from the usual, this time, we also have a few other variables, such as velocity
, jumpForce
, gravity
, and drag
.
To make sure that the projectile does a good job of hitting the player, we will make use of some math and physics. We will use the projectile equation to calculate with how much force the projectile should be launched to get a successful target hit.
To get the exact force, we will set the angle of attack, gravity, and drag. So, let's go into the implementation and set the values, as follows:
#import "ProjectileAI.h" @implementation ProjectileAI -(id)initWithFilename:(NSString *) filename Hero:(CCSprite*) hero pos: (CGPoint) position Direction: (int)Direction { if (self = [super initWithImageNamed:filename]) { _hero = hero; xDirection = Direction; speed = 10.0f; [self setPosition:position]; startPos = self.position; gravity = ccp(0, -120); drag = ccp(1.0, 1.0); isAlive = true; float distX = hero.position.x - self.position.x; float distY = hero.position.y - self.position.y; distX = fabsf(distX); // ** angle constant float shootAngle = 45.0f; shootAngle = - CC_DEGREES_TO_RADIANS(shootAngle); float angle2= sin(2 * shootAngle); float force = sqrtf((distX * gravity.y )/(angle2)); CCLOG("force: %f", force); shootAngle = - CC_RADIANS_TO_DEGREES(shootAngle); jumpForce = ccp(xDirection * force * cosf(shootAngle), force * sinf(shootAngle)); velocity = ccpAdd(velocity, jumpForce); } return self; }
In the init
function, we will set the usual variables, such as speed
, direction
, and position
. Then, we will set gravity
and drag
.
As gravity
always acts downwards, we only set the Y
direction. The value of gravity
needs to be tinkered with to make sure we get an exact hit on the player. The drag accounts for air friction, and the values are set to 1
in the x
and y
directions to ignore any air friction. This can be modified according to the needs of your game.
Then, we will set the isAlive
variable to true
.
We will then calculate the distance in the x
and y
directions separately. Then, we will get the absolute value in the x
direction as we are only concerned with the distance and don't care whether it is positive or negative.
To calculate the force, we will first set the shooting angle to 45 degrees. Then, we will calculate it in radians and multiply it by -1.
A new variable called value
will be created, which stores the value of sin(2 * angle)
. Then, the formulae for calculating the force will be given by the square root of the distance in the x
direction multiplied by the gravity in the y
direction and divided by the sin(2 * angle)
value. As we already calculated the value of sin(2 * angle)
, we will just divide it by this to make it easier.
Now, to actually apply the force to the projectile, we have to first convert the angle back to degrees and multiply it by -1.
Now, the jump force or initial velocity of the projectile in the x
direction is the direction in which the projectile needs to be launched multiplied by the force, further multiplied by the cosine of the angle. The force in the y
direction is given by the force multiplied by the sine of the angle. Finally, for the resultant initial velocity the calculated jump force is added to it. Take a look at the following code:
- (void)update:(CCTime)delta { CGPoint mGravityStep = ccpMult(gravity, delta); velocity = ccpAdd(velocity, mGravityStep); CGPoint moveSpeed = ccp(xDirection * speed, 1 * speed); CGPoint moveSpeedStep = ccpMult(moveSpeed, delta); velocity = ccpAdd(velocity, moveSpeedStep); CGPoint mVelocityStep = ccpMult(velocity, delta); CGPoint initpos = self.position; CGPoint finalpos = self.position = ccpAdd(self.position, mVelocityStep); float dx = finalpos.x - initpos.x; float dy = finalpos.y - initpos.y; float rotAngle = atan2f(dy, dx); rotAngle = CC_RADIANS_TO_DEGREES( - rotAngle); [self setRotation:rotAngle]; if(self.position.y < 75 && isAlive){ isAlive = false; [self removeFromParent]; } if(isAlive){ if(CGRectIntersectsRect(self.boundingBox, _hero.boundingBox)){ isAlive = false; [self removeFromParent]; } } } @end
In the update
function, to make the gravity independent of the processor speed, we will calculate gravityStep
, which is the multiplication of gravity by delta time. The gravityStep
variable is added to the velocity.
Then, the current movement speed is calculated. For the x
direction, we will multiply it by the direction in which we want the projectile to move. Then, the moveStep
variable will be calculated by multiplying the speed by the delta time. The moveStep
variable is then added to the velocity, and velocityStep
is calculated.
Finally, the velocity is added to the current position to get the final position of the projectile.
In order for the projectile to rotate properly and face the direction of movement, we have to do a little more calculation.
To calculate the rotation angle, we will first get the change in distance between the previous frame and the next frame in both the x
and y
directions. Then, we will calculate the slope by calculating the tan inverse of the distance in the y
direction divided by the distance in the x
direction.
We will multiply the angle by -1 and then convert the value from radians to degrees.
Finally, we will set the current rotation of the object to the newly calculated value.
After this, we will first check whether the current y
position is less than 75
; if it is, then we will set the isAlive
Boolean to false
and remove the self from the parent.
Next, we will check for the collision between the player and projectile. If true
, then we will again set the isAlive
Boolean to false
and remove the current class from the parent.
To see our projectile in action, we have to import the ShooterBase.h
header into the MainScene.h
header.
Now, after PatrolAI
, we will add the following and comment out the PatrolAI
code as it is not required any more:
CGPoint startPos = CGPointMake(winSize.width * .5 , winSize.height/4); //PatrolAI* pEnemy = [[PatrolAI alloc] initWithFilename:| @"enemy.png" :hero :startPos]; //[self addChild: pEnemy]; ShooterBase* shooterBase = [[ShooterBase alloc]initWithFilename:@"RocketBase.png" Hero:hero pos:startPos Direction: 1.0]; [self addChild:shooterBase];
3.135.187.210