Developers Manual > Cocos2d-x > ShowCase > Fantasy Warrior 3D

Fantasy Warrior 3D

version: Cocos2d-x v3.3
update: Updated over 2 years ago

This doc aims to help you analyze the structure of the FantasyWarrior3D. You will gain a better understanding of FantasyWarrior3D by reading this.

The FantasyWarrior3D is based on Cocos2d-x 3.3 Lua. And we used Cocos Code IDE. There are 4 scenes in FantasyWarrior3D:

The most important scene of FantasyWarrior3D is the BattleScene.

The structure of BattleScene:

The BattleScene has only one layer. All other elements were added to the currentLayer, including the heroes and enemies. The camera is used for controlling the angles or the ranges of the view. The uiLayer is a child of the camera. I will talk about this in Camera track.

Most of the game logics are under control by the gameController. It's a scheduler that contains the collision detection, attack command, camera track, touch events and main logic. These functions will be called of every frame in FantasyWarrior3D.

Collision Detection

collisionDectect is the first function called in the gameController. Its target is to check all of the actors in the game. If they got too closer, push to separate, or pull they back when they out of the scene edges.

Each actor has a circular region. The size of the region is decided by the _radius. It also has a _mass property. All the actors in the scene were put in the HeroManager and MonsterManager. The logic of the collision detection is simple:

  • Loop the actors, and then calculate the distance between every actor to the rest actors. If the distance was less than the sum of the radius of the 2 actors, we call it the collision is true.
  • If the collision is true, recalculate the distance of the actors should be. Then update their positions.
  • Function isOutOfBound would check if the actors out of the bound after the collision detection. And push them back if they were not.
  • Collision detection will skip the dead actors in the game. The HeroManager and MonsterManager will remove them.

Camera Track

We have two kinds of camera track in FantasyWarrior3D:

  1. Camera that keeps moving with the 3 heroes. It can also change the location when player’s touch moves on the screen.
  2. Close-up camera when heroes use their special skills.

Both of the cameras were implemented in the moveCamera function. The camera changes its position by setPosition3D constantly, and uses lookAt function to adjust the angles of camera to make it look at the right place.

When the camera needs to setPosition3D or lookAt from A to B, you cannot just setPosition3D(B) or lookAt(B). In FantasyWarrior3D, we used cc.pLerp(A,B) to make it more smoothly.

In onTouchMoved of the BattleScene, camera will change its position by the cameraOffset. And players will change the cameraOffset when the "touch moved" the screen. In this way players can control the camera in a limited range.

Finally, we can add the uiLayer to the camera (addChild). That is because if we added the uiLayer to the BattleScene, it would be lost when the camera start to move. But once it was added on the camera, and set a suitable position and distance from the camera, the camera could see the uiLayer wherever it moves to. Just like wearing glasses.

Here're some pictures to help you understand the camera in a 3D area:

The left picture shows the default position of the camera. The green arrow is X-axes, blue is Y-axes and green is Z-axes. Its position is (winSize.width/2, winSize.height/2, getZEye), and it looks to the direction of (winSize.width/2, winSize.height/2). You can read the codes of Camera::initDefault() in Cocos2d-x below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
bool Camera::initDefault()
{
    auto size = Director::getInstance()->getWinSize();
    //create default camera
    auto projection = Director::getInstance()->getProjection();
    switch (projection)
    {
        case Director::Projection::_2D:
        {
            initOrthographic(size.width, size.height, -1024, 1024);
            setPosition3D(Vec3(0.0f, 0.0f, 0.0f));
            setRotation3D(Vec3(0.f, 0.f, 0.f));
            break;
        }
        case Director::Projection::_3D:
        {
            float zeye = Director::getInstance()->getZEye();
            initPerspective(60, (GLfloat)size.width / size.height, 10, zeye + size.height / 2.0f);
            Vec3 eye(size.width/2, size.height/2.0f, zeye), center(size.width/2, size.height/2, 0.0f), up(0.0f, 1.0f, 0.0f);
            setPosition3D(eye);
            lookAt(center, up);
            break;
        }
        default:
            CCLOG("unrecognized projection");
            break;
    }
    return true;
}

The right picture is the second kind of camera. It will keep adjusting its position according to the heroes' position, but its Z-axes would never change.

UI Layer

The uiLayer shows the heroes' avatars, HP and anger. The avatars will shake when the heroes are under attack. It will also turn grey if the hero is dead. The players can use special skills by touching the avatars if the anger is full.

The UI logic is separated from the game logic. And we use a way called registerMessage\dispatchMessage to listen and deliver the events during the battle. For example, in BattleScene, we register the messages of BLOOD_MINUS and ANGRY_CHANGE to listen the events when the HP is dropped or angry is changed. It will callback the bloodminus and angryChange functions and send the information to the uiLayer to change the HP and angry.

Actors

The actors are under the folder of actors:

Each actor has its own update function, which would be called at every frame of game. The update deals with the logics below:

  • The baseUpdate function, in charge of the AI logics
  • The stateMachineUpdate function, update the actors' animations, actions by their statement
  • The movementUpdate function, update the positions and facings of actors

StateMachines of Actors

The statements of actors are defined in the GlobalVariables.lua, consisting of these sections:

The statements of actors were under control of the stateMachineUpdate, keeping transformed in six statements above. The flow of the transforming were:

walkUpdate: The logic of walking is, if the actor has one target within the attack range, the actor would turn to attackMode; If the target is outside the attack range, it can keep walking until the end of the map. When the actors arrive at the end of the scene, it will change to idle mode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Actor:walkUpdate(dt)
    --Walking state, switch to attack state when target in range
    if self._target and self._target._isalive then
        local attackDistance = self._attackRange + self._target._radius -1
        local p1 = self._myPos
        local p2 = self._target._myPos
        self._targetFacing = cc.pToAngleSelf(cc.pSub(p2, p1))
        --print(RADIANS_TO_DEGREES(self._targetFacing))
        if cc.pGetDistance(p1,p2) < attackDistance then
            --we are in range, lets switch to attack state
            self:attackMode()
        end
    else
        --our hero doesn't have a target, lets move
        --self._target = self:_findEnemy(self._raceType)
        local curx,cury = self:getPosition()
        if self._goRight then
            self._targetFacing = 0
        else
            self:idleMode()
        end
    end
end

attackUpdate: The attack logic was divided to 2 types: normal attack and special attack. So the logic will first check if it is time for attacking, and then choose normal attack or special attack and play the corresponding animation. If it were the special attack, the attackUpdate would do 3 other things below:

  • Dark down the view of the game
  • Move the camera, give the hero a close-up
  • Slow down the scheduler by setTimeScale

No matter what kind of attack, it would send a attackCommand, which we will talk about later.

knockingUpdate: The actors will play the knocking animation when being hurt. After the animation is over, the actor will judge if the enemy is inside the attack range to choose transform to attackMode or walkMode

Actors' AI

The actors' AI is driven by the baseUpdate. Each actor has his/her own _AIFrequency. So the basic logic of AI is: If it is time to execute the next AI, if so, execute it.

The main logic of the AI is to find out the target. If there is no target, set to the walkMode or idleMode. If the target is appeared, change to attackMode if it is under the range, or change to walkMode to get closer.

Attack Command

Attack command is an independent class, its baseClass is BasicCollider:

The sp means the spikes, ice or arrows. It a sprite or a Sprite3D instance. But those are just some effects. The real attack is the attack range of the BasicCollider. It can be a circle or a sector.

minRange defines the minimum attack radius, it could be 0; maxRange is larger than minRange; Angle defines the central angle of the sector, the range is (0,360]; Facing defines the direction; All these properties construct the area of attack. The difference between A and B in the pictures is that the minRange of A is zero, when the target inside the A or B, it would be under attack.

When attack command is created, it would be added into the AttackManager(BasicCollider:initData). And the solveAttacks would loop the AttackManager, every time it takes out an attack command, it will check if the actor has a collision; if not, continue the loop. If yes, run the onCollide, play the attack audio effects and execute the hurt function. Before the loop is finished, remov the invalid attack commands (which means curDuration>duration). The attack commands also have 2 types: normal attack and special attack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function solveAttacks(dt)
    for val = AttackManager.last, AttackManager.first, -1 do
        local attack = AttackManager[val]
        local apos = getPosTable(attack) 
        if attack.mask == EnumRaceType.HERO then
            --if heroes attack, then lets check monsters
            for mkey = MonsterManager.last, MonsterManager.first, -1 do
                --check distance first
                local monster = MonsterManager[mkey]
                local mpos = monster._myPos
                local dist = cc.pGetDistance(apos, mpos)
                if dist < (attack.maxRange + monster._radius) and dist > attack.minRange then
                    --range test passed, now angle test
                    local angle = radNormalize(cc.pToAngleSelf(cc.pSub(mpos,apos)))
                    local afacing = radNormalize(attack.facing)

                    if(afacing + attack.angle/2)>angle and angle > (afacing- attack.angle/2) then
                        attack:onCollide(monster)
                    end
                end
            end
        elseif attack.mask == EnumRaceType.MONSTER then
            --if heroes attack, then lets check monsters
            for hkey = HeroManager.last, HeroManager.first, -1 do
                --check distance first
                local hero = HeroManager[hkey]
                local hpos = hero._myPos
                local dist = cc.pGetDistance(getPosTable(attack), hpos)
                if dist < (attack.maxRange + hero._radius) and dist > attack.minRange then
                    --range test passed, now angle test
                    local angle = cc.pToAngleSelf(cc.pSub(hpos,getPosTable(attack)))
                    if(attack.facing + attack.angle/2)>angle and angle > (attack.facing- attack.angle/2) then
                        attack:onCollide(hero)
                    end
                end
            end
        end
        attack.curDuration = attack.curDuration+dt
        if attack.curDuration > attack.duration then
            attack:onTimeOut()
            List.remove(AttackManager,val)
        else
            attack:onUpdate(dt)
        end
    end
end

Main Logic

The main logic is driven by GameMaster:update. This function will be executed in the gameController of the BattleScene.

The main logic contains two functions: first, it creates the heroes and monsters; second, it allows the monsters and interfaces to appear in appropriate timing.

By the way, the game will drop frames when creating monsters, so we put all the monsters in a pool when created, then added them to the currentLayer and set visible to be false. Only when the they are needed, we would get them out, and send to the MonsterManger.

gamecontroller.jpeg (21.2 kB) zijian_rao, 2014-11-06 06:32

scenes.jpg (66.8 kB) zijian_rao, 2014-11-06 06:32

statement.jpeg (23.9 kB) zijian_rao, 2014-11-06 06:32

transforming.jpeg (33.7 kB) zijian_rao, 2014-11-06 06:32

actors.jpeg (24.8 kB) zijian_rao, 2014-11-06 06:32

attackRange.jpeg (27.2 kB) zijian_rao, 2014-11-06 06:32

BasicCollider.jpeg (31.9 kB) zijian_rao, 2014-11-06 06:32

battleScene.jpeg (36.6 kB) zijian_rao, 2014-11-06 06:32

update.jpeg (13.5 kB) zijian_rao, 2014-11-06 06:32

camera.jpeg (117.1 kB) zijian_rao, 2014-11-06 06:32

Sign up for our newsletter to keep up with the latest developments, releases and updates for Cocos2d-x.