[Excellent Tutorial] Making an Archery Game with Cocos2d-x v3.6 (Part 2)

[Excellent Tutorial] Making an Archery Game with Cocos2d-x v3.6 (Part 2)

Our main task in this chapter is to create an archer (the protagonist of the game) who can shoot arrows and let the protagonist rotate the bow and arrow in his hand continuously as the touch point changes.

analyze:

For this archery character, it can shoot arrows continuously. When we press a certain point on the screen, a red line (looking like a parabola) will be "drawn" from the position of the character's hand holding the arrow to mark the movement trajectory of the arrow; when sliding the finger or mouse on the screen, the red line will change its trajectory continuously with the position of the touch point; when releasing the finger or mouse on the screen, an arrow will be shot, and this arrow will move along the final red line path. In addition, the bow and arrow in the player's hand will rotate with the finger or mouse on the screen.

Player Class

Let's create the Player class. Its initial definition is as follows:

  1. class Player: public Sprite
  2. {
  3. public :
  4. Player();
  5.    
  6. bool init(Vec2 playerPos);
  7. static Player* create(Vec2 playerPos);
  8.    
  9. void createPlayer();
  10. void createPlayerHpBar();
  11. void rotateArrow(Point touchPoint);
  12. void createAndShootArrow( Point touchPoint);
  13. void shootArrow();
  14. void finishRunAction();
  15. void update( float dt);
  16.    
  17. CC_SYNTHESIZE( int , playerHp, PlayerHp); // Player health value  
  18. CC_SYNTHESIZE(bool, startDraw, StartDraw); // Start drawing the red path line  
  19. CC_SYNTHESIZE(bool, isRunAction, IsRunAction); // Is the player performing the archery animation?  
  20.    
  21. private :
  22. Vec2 playerPos; // The position of the character on the tmx map  
  23. Size playerSize; // Character size  
  24. Size winSize; //Screen window size  
  25. Sprite* playerbody; // character body  
  26. Sprite* playerarrow; // The character's bow and arrow, that is, the bow and arrow part that will rotate with the touch point  
  27. Sprite* hPBgSprite; // Character health bar background sprite  
  28. ProgressTimer* hpBar; // Character health bar  
  29. ccQuadBezierConfig bezier; // Path Bezier  
  30. DrawNode* drawNode; // This represents our line object  
  31.    
  32. };

The above methods are what we need to implement in these two chapters. We will expand other methods when needed later.

The CC_SYNTHESIZE macro is used to define a protected variable and declare a getfunName function and a setfunName function. You can use the getfunName function to get the value of the variable and use the setfunName function to set the value of the variable. For example: CC_SYNTHESIZE(int, playerHp, PlayerHp); defines an integer playerHp variable and also declares two methods, getPlayerHp() and setPlayerHp().

ccQuadBezierConfig is a newly defined structure, which we will explain in detail later.

Let’s take a look at each of the above methods from top to bottom.

Create a character

The first step is to initialize (init) and create (create) the Player. Here we create the character by giving the Player's position, and the incoming coordinate position should be the position we read from the object layer of TiledMap (described in the previous chapter). The specific code is as follows:

  1. Player * Player::create(Vec2 playerPos)
  2. {
  3. Player *pRet = new Player();
  4. if (pRet && pRet->init(playerPos))
  5. {
  6. pRet->autorelease();
  7. return pRet;
  8. } else  
  9. {
  10. delete pRet;
  11. pRet = NULL;
  12. return NULL;
  13. }
  14. }
  15. bool Player::init(Vec2 playerPos)
  16. {
  17. if (!Sprite::init())
  18. {
  19. return   false ;
  20. }
  21. this- >playerPos = playerPos;
  22. createPlayer(); // Create a character  
  23. createPlayerHpBar(); // Create a character health bar  
  24. scheduleUpdate();
  25. return   true ;
  26. }

Next, let's take a look at the createPlayer method, which will initialize our Player character. The code is as follows:

  1. void Player::createPlayer()
  2. {
  3. playerbody = Sprite::createWithSpriteFrameName( "playerbody.png" );
  4. playerSize = Size(playerbody->getContentSize().width/ 2 , playerbody->getContentSize().height / 3 * 2 );
  5. // Set the size of the Player to be slightly smaller than the size of the playerbody, so that we can set the collision more accurately later.  
  6. playerbody->setAnchorPoint(Vec2( 0 .7f, 0 .4f));
  7. this- >addChild(playerbody);
  8. this ->setPosition(Vec2(playerPos.x+ GameManager::getInstance()->getObjectPosOffX(), playerPos.y + playerSize.height * 0 .4f));
  9.    
  10. playerarrow = Sprite::createWithSpriteFrameName( "playerarrow.png" );
  11. playerarrow->setPosition(Vec2( 0 , 0 ));
  12. playerarrow->setAnchorPoint(Vec2( 0 .3f, 0 .5f));
  13. this- >addChild(playerarrow);
  14. }

In the createPlayer method, we will create a game character as shown below.

Because we couldn't find suitable game resources (the resources obtained in the original game were all parts, and they had to be reassembled frame by frame to be used), our game was kept simple and we didn't include complicated ones.

Here we simply divide the character into two parts. The first part is of course the player's body, and the second part is the hand and the arrow that rotates with the touch point/mouse. (PS: Of course, due to resource limitations, the quality of our game may be slightly reduced, but you can't blame me! O(∩_∩)O~)

When setting the playerbody position, you may have noticed that we did not set the character's body at the passed in playerPos, but slightly adjusted it. This is because the position we passed in is close to the bottom of the tile (we need to do this when making a tmx file. I didn't explain it clearly in the last chapter, so I'll make it up in this chapter. Remember it!). As shown in the following figure:

The Y value coordinate should not be too close to the bottom of the tile, that is, do not set it to 9.990, 9.998, which is too close to 10, because the coordinate values ​​stored in the tmx file are integers. If it is set to 9.990, 9.998, then the stored value will be 9.990 X 32 = 319.68 = 320, and similarly 9.998 X 32 is also 320. 320 is a special number for a map with a tile size of 32 X 32, because 320 / 32 = 10. In this way, the program will mistakenly think that points such as 9.990 and 9.998 are the 10th point on the coordinate.

And as we said in the previous chapter, due to resolution adaptation, there is a certain deviation between the position of objects in the object group and the actual position, so we need to correct these deviations when setting the character's body position.

The schematic diagram for setting the position in the above code is as follows:

The offset value of the object group on the X axis is stored in GameManager, which is a singleton class. We will explain it in detail in the following chapters. Of course, if you want to run the code now, remove the GameManager::getInstance()->getObjectPosOffX() part first.

After creating the character, we need to create the character's health bar. The health bar can be created through the ProgressTimer class encapsulated in Cocos2d-x. The code snippet is as follows:

  1. void Player::createPlayerHpBar()
  2. {
  3. // Create the health bar bottom, which is the bottom background of the progress bar  
  4. hPBgSprite = Sprite::createWithSpriteFrameName( "hpbg.png" );
  5. hPBgSprite->setPosition(Vec2(playerbody->getContentSize().width / 2 , playerbody->getContentSize().height));
  6. playerbody->addChild(hPBgSprite);
  7. // Create a health bar  
  8. hpBar = ProgressTimer::create(Sprite::createWithSpriteFrameName( "hp1.png" ));
  9. hpBar->setType(ProgressTimer::Type::BAR); // Set the progress bar style (bar or ring)  
  10. hpBar->setMidpoint(Vec2( 0 , 0 .5f)); // Set the starting point of the progress bar, (0, y) means the leftmost, (1, y) means the rightmost, (x, 1) means the topmost, and (x, 0) means the bottommost.  
  11. hpBar->setBarChangeRate(Vec2( 1 , 0 )); // Set the direction of progress bar change, (1,0) represents horizontal direction, (0,1) represents vertical direction.  
  12. hpBar->setPercentage( 100 ); // Set the current progress bar's progress  
  13. hpBar->setPosition(Vec2(hPBgSprite->getContentSize().width / 2 , hPBgSprite->getContentSize().height / 2 ));
  14. hPBgSprite->addChild(hpBar);
  15. hPBgSprite->setVisible( false ); // Set the entire health bar invisible. We will display the health bar when the Player is attacked.  
  16. }

#p#

Rotate character bow

Next, we will let the bow and arrow of the Player rotate according to the touch point/mouse. So we define the following function:

  1. void Player::rotateArrow(Point touchPoint)
  2. {
  3. // 1  
  4. auto playerPos = this ->getPosition();
  5. auto pos = playerPos + playerarrow->getPosition();
  6. // 2  
  7. Point vector = touchPoint - pos;
  8. auto rotateRadians = vector.getAngle();
  9. auto rotateDegrees = CC_RADIANS_TO_DEGREES( - 1 * rotateRadians);
  10. // 3  
  11. if (rotateDegrees >= - 180 && rotateDegrees <= - 90 ){
  12. rotateDegrees = - 90 ;
  13. }
  14. else   if (rotateDegrees >= 90 && rotateDegrees <= 180 ){
  15. rotateDegrees = 90 ;
  16. }
  17. // 4  
  18. auto speed = 0.5 / M_PI;
  19. auto rotateDuration = fabs(rotateRadians * speed);
  20. // 5  
  21. playerarrow->runAction( RotateTo::create(rotateDuration, rotateDegrees));
  22. }

The parameter of the rotateArrow method is the location of the touch point.

1) Get the position of the character's bow and arrow in the game scene;

2) Calculate the rotation angle of the bow and arrow.

The trigonometric tangent function is used here for calculation, and the principle is shown in the following figure:

vector(offX, offY) is the vector between the touch point and the bow. Through the getAngle method, we can get the arc between the vector and the X axis.

Furthermore, we need to convert radians rotateRadians to degrees. CC_RADIANS_TO_DEGREES is a macro that can convert radians to degrees. The reason for multiplying by -1 during conversion is that Cocos2d-x stipulates that the clockwise direction is positive, which is opposite to the direction of the angle we calculated, so the angle a needs to be converted to -a during conversion.

3) Control the range of the rotation angle, that is, only let it rotate within the right half of the character.

4) Calculate the arrow rotation time.

speed indicates the speed at which the turret rotates. 0.5 / M_PI is actually 1 / 2PI, which means it rotates one circle in 1 second.

rotateDuration represents the time required to rotate a specific angle, which is calculated by multiplying radians by speed.

5) Make the bow and arrow perform a rotational motion.

Touch Response

OK, now the Player is preliminarily defined. Next, we return to the game scene, add the Player to it, and test whether the arrow rotates with the touch point.

In the Cocos2d-x 3.x engine, the process of implementing touch response is basically the same. So in 3.6, the process is still:

  • Override touch callback function;
  • Create and bind touch events;
  • Implement the touch callback function.

So we want to test whether the arrow rotates with the touch point. The first step is to rewrite the following touch callback function in GameScene and declare the variable:

  1. virtual bool onTouchBegan(Touch *touch, Event *unused_event); // respond when the screen starts to be touched  
  2. virtual void onTouchMoved(Touch *touch, Event *unused_event); // Respond when touching and sliding the screen  
  3. virtual void onTouchEnded(Touch *touch, Event *unused_event); // Response when touch ends  
  4.    
  5. private :
  6. Point preTouchPoint; //Previous touch point  
  7. Point currTouchPoint; // Current touch point  

Next, we need to create and bind touch events in the init function of GameScene, and create a Player object for testing. As follows:

  1. SpriteFrameCache::getInstance()->addSpriteFramesWithFile( "texture.plist" , "texture.pvr.ccz" );
  2. player = Player::create(Vec2(winSize.width / 4 , winSize.height/ 5 ));
  3. this- >addChild(player);
  4.    
  5. // Get the event dispatcher  
  6. auto dispatcher = Director::getInstance()->getEventDispatcher();
  7. // Create a single touch listener  
  8. auto listener = EventListenerTouchOneByOne::create();
  9. // Let the listener bind the event processing function  
  10. listener->onTouchBegan = CC_CALLBACK_2(GameScene::onTouchBegan, this );
  11. listener->onTouchMoved = CC_CALLBACK_2(GameScene::onTouchMoved, this );
  12. listener->onTouchEnded = CC_CALLBACK_2(GameScene::onTouchEnded, this );
  13. // Add the event listener to the event dispatcher  
  14. dispatcher->addEventListenerWithSceneGraphPriority(listener, this );

The position of the Player is fixed, so we can't set it randomly. This is just for testing. In the following chapters, we will create a class to manage objects obtained from TiledMap, including Player, enemies, props, bricks, etc.

The above plist and pvr.ccz files are our packaged resources, which are packaged using the Texturepacker editor. Click here for more details.

After binding the touch events, we finally need to implement them. The code is as follows:

  1. bool GameScene::onTouchBegan(Touch *touch, Event *unused_event)
  2. {
  3. currTouchPoint = touch->getLocation();
  4. if (!currTouchPoint.equals(preTouchPoint)){
  5. player->rotateArrow(currTouchPoint);
  6. }
  7. preTouchPoint = currTouchPoint;
  8. return   true ;
  9. }
  10.    
  11. void GameScene::onTouchMoved(Touch *touch, Event *unused_event)
  12. {
  13. currTouchPoint = touch->getLocation();
  14. if (!currTouchPoint.equals(preTouchPoint)){
  15. player->rotateArrow(currTouchPoint);
  16. }
  17. preTouchPoint = currTouchPoint;
  18. }
  19.    
  20. void GameScene::onTouchEnded(Touch *touch, Event *unused_event)
  21. {
  22. // Archery, next chapter  
  23. }

In the onTouchBegan and onTouchMoved functions, the processing method is the same, that is, when the current touch point is inconsistent with the previous touch point, the Player's bow and arrow are rotated.

The getLocation method converts the screen coordinates saved in the touch object into the Cocos2d coordinates we need. For those who cannot distinguish between screen coordinates and Cocos2d coordinates, please refer to the article Detailed Explanation of the Cocos2d-x3.0 Coordinate System.

When the touch ends, the Player object needs to shoot the arrow, but we won't write this for now.

Run the game and you can see the desired effect. For resources in this chapter, please click here to download.

<<:  A brief analysis of the conspiracy theory behind the App Store's 1 Yuan selection

>>:  Learn more about IOS9 every day 2: UI testing

Recommend

Quantum interference makes counting stars easier

The exploration of the deep night sky has been wi...

Tencent's love-style product operation method

What does it feel like to be in love? Most of the...

Lollipop 5.0: Eight ways to reinvent Android

[[123481]] Security has been enhanced, the overal...

Yoga Beginner Video

Introduction to Yoga Beginner Video Resources: Co...

How much does it cost to develop a watch applet in Alxa League?

How much does it cost to develop the Alxa League ...

NTT: 2022 Connected Industry Report

Industry 4.0 is a term that describes initiatives...

Dismantling the "Hot Pot Restaurant" Gamification Private Domain Community Case

The Internet circle is familiar with the concept ...

What do subway tunnels look like? Why are most of them round?

Have you ever been curious? What does the subway ...

New firmware causes trouble: MacBook Air may become bricked

Apple released the EFI 2.9 update for the MacBook ...

Mother's Day Marketing Guide

There is still a month to go until Mother's D...