跳转至

09-玩家类实现和角色移动基础

1 Player类定义

由于不同玩家的操作方式类似,但是效果不同。所以可以类比Scene类,定义一个Player基类,来作为不同角色玩家类型生成的基础。

class Player
{
public:
    Player() = default;
    ~Player() = default;

    virtual void on_update(int delta)
    {

    }

    virtual void on_draw(const Camera& camera)
    {

    }

    virtual void on_input(const ExMessage& msg)
    {

    }
private:

};

1.1 子类PeashooterPlayer

class PeashooterPlayer : public Player
{
public:
    PeashooterPlayer() = default;
    ~PeashooterPlayer() = default;

    void on_update(int delta)
    {
        std::cout << "婉豆射手正在更新..." << std::endl;
    }
private:
};

1.2 子类SunflowerPlayer

class SunflowerPlayer : public Player
{
public:
    SunflowerPlayer() = default;
    ~SunflowerPlayer() = default;

    void on_update(int delta)
    {
        std::cout << "龙日葵正在更新..." << std::endl;
    }
private:
};

2 实例化

游戏中需要实例化哪个玩家角色类,受到了玩家角色选择地控制.因此,我们需要在玩家角色选择界面执行这部分逻辑.
而且由于它的声明周期跨越多个场景类,所以我们把它定义在全局环境中.

首先来到main.cpp中

Player *player_1 = nullptr;
Player *player_2 = nullptr;

定义两个玩家角色的指针

之后再SelectorScene::on_exit()中,根据玩家选择的角色类型,实例化不同的角色子类对象

switch (player_type_2) 
{
case PlayerType::Peashooter:
    player_2 = new PeashooterPlayer();
    break;
case PlayerType::Sunflower:
    player_2 = new SunflowerPlayer();
    break;
}

之后在GameScene引入对应头文件和变量之后,在on_update()中对玩家角色进行更新

void on_update(int delta) {
    player_1->on_update(delta);
    player_2->on_update(delta);
}

3 角色移动

我们在Player类中添加表示位置的私有变量

private:
    Vector2 position;

之后,我们添加玩家角色的动画相关的定义,再回头编写修改玩家位置的逻辑

Animation animation_idle_left;      // 向左的默认动画
Animation animation_idle_right;     // 向右的默认动画
Animation animation_run_left;       // 向左奔跑动画
Animation animation_run_right;      // 向右奔跑动画

3.1 区分玩家序号

让我们想一想,有哪些共有的逻辑可以写到Player基类中。显然,读取玩家的按键逻辑,并将按键消息映射到对应的逻辑的这部分代码。那如何应对不同玩家的键位不同呢?

我们把玩家序号作为成员,只需要再按键操作时根据序号执行不同的逻辑即可。
所以我们创建player_id.h文件,在其中定义PlayerID枚举类

enum class PlayerID
{
    P1,
    P2
};

在Player类中定义id变量
PlayerID id = PlayerID::P1;     //玩家序号ID

同时编写set_id()方法
void set_id(PlayerID id)
{
    this->id = id;
}

在SelectorScene::on_exit()方法中实例化玩家角色之后,设置它们的id

player_1->set_id(PlayerID::P1);
player_2->set_id(PlayerID::P2);

3.2 处理不同键位

在提瓦特幸存者项目中,我们认识到直接使用按键消息来对角色位置进行累加是不合适的。所以,我们使用按键状态的方式来处理移动操作。
在Player类中添加反映按键状态的bool变量。

bool is_left_key_dwon = false;      //向左移动按键是否按下 
bool is_right_key_down = false;     //向右移动按键是否按下

之后在on_input方法中,添加处理按键输入的逻辑.
由于使用了多层switch语句,所以一定要检查好break是不是漏写了
virtual void on_input(const ExMessage& msg)
{
    switch (msg.message)
    {
    case WM_KEYDOWN:
        switch (id)
        {
        case PlayerID::P1:
            switch (msg.vkcode)
            {
            case 'A':
                is_left_key_dwon = true;
                break;
            case 'D':
                is_right_key_down = true;
                break;
            default:
                break;
            }
            break;
        case PlayerID::P2:
            switch (msg.vkcode)
            {
            case VK_LEFT:
                is_left_key_dwon = true;
                break;
            case VK_RIGHT:
                is_right_key_down = true;
                break;
            default:
                break;
            }
            break;
        default:
            break;
        }
        break;
    case WM_KEYUP:
        switch (id)
        {
        case PlayerID::P1:
            switch (msg.vkcode)
            {
            case 'A':
                is_left_key_dwon = false;
                break;
            case 'D':
                is_right_key_down = false;
                break;
            default:
                break;
            }
            break;
        case PlayerID::P2:
            switch (msg.vkcode)
            {
            case VK_LEFT:
                is_left_key_dwon = false;
                break;
            case VK_RIGHT:
                is_right_key_down = false;
                break;
            default:
                break;
            }
            break;
        default:
            break;
        }
        break;
    default:
        break;
    }
}

3.3 渲染玩家动画

现在,我们来到on_update中,通过按键作差的方式判断玩家此时的移动方向。

由于玩家不进行任何按键操作时,角色动画会保持之前的状态,不会进行任何翻转操作。所以我们不能直接使用这里计算的移动方向来判断角色动画的播放朝向。而是应该在私有变量中添加一个bool变量is_facing_right用来表示玩家角色是否朝向右侧。

bool is_facing_right;

virtual void on_update(int delta)
{
    int direction = is_right_key_down - is_left_key_dwon;

    if(direction != 0)
    {
        is_facing_right = direction > 0;
    } else {

    }
}

我们定义一个Animation指针,指向当前正在播放的动画对象
Animation *current_animation;       //当前正在播放的动画

virtual void on_update(int delta)
{
    int direction = is_right_key_down - is_left_key_dwon;

    if(direction != 0)
    {
        is_facing_right = direction > 0;
        current_animation = is_facing_right ? &animation_run_right : &animation_run_left;
    } else {
        current_animation = is_facing_right ? &animation_idle_right : &animation_idle_left;
    }

    current_animation->on_update(delta);
}

最后在on_draw中,绘制当前动画
virtual void on_draw(const Camera& camera)
{
    current_animation->on_draw(camera, (int)position.x, (int)position.y);
}

我们还需要在更上层的GameScene中调用Player的on_draw方法

player_1->on_draw(camera);
player_2->on_draw(camera);

还要在on_input中调用player的对应方法
player_1->on_input(msg);
player_2->on_input(msg);

3.4 实现子类中的逻辑

我们来到子类PeashooterPlayer,先引入要用到的Atlas图集资源

extern Atlas atlas_peashooter_idle_left;
extern Atlas atlas_peashooter_idle_right;
extern Atlas atlas_peashooter_run_left;
extern Atlas atlas_peashooter_run_right;

修改构造函数
PeashooterPlayer() {
    animation_idle_left.set_atlas(&atlas_peashooter_idle_left);
    animation_idle_right.set_atlas(&atlas_peashooter_idle_right);
    animation_run_left.set_atlas(&atlas_peashooter_run_left);
    animation_run_right.set_atlas(&atlas_peashooter_run_right);
};

这里子类PeashooterPlayer使用了基类Player中的变量。要把这些变量由private设置为protected

类似的,我们在SunflowerPlayer中也导入要用到的图集资源并进行Animation的设置

extern Atlas atlas_sunflower_idle_left;
extern Atlas atlas_sunflower_idle_right;
extern Atlas atlas_sunflower_run_left;
extern Atlas atlas_sunflower_run_right;

SunflowerPlayer() {
    animation_idle_left.set_atlas(&atlas_sunflower_idle_left);
    animation_idle_right.set_atlas(&atlas_sunflower_idle_right);
    animation_run_left.set_atlas(&atlas_sunflower_run_left);
    animation_run_right.set_atlas(&atlas_sunflower_run_right);

    animation_idle_left.set_interval(75);
    animation_idle_right.set_interval(75);
    animation_run_left.set_interval(75);
    animation_run_right.set_interval(75);
}

另外,前面我们其实忘记了给Player类中的current_animation赋值,即设置默认显示的动画,所以我们要在Player的构造函数中添加下面这行代码
current_animation = &animation_idle_right;

3.5 位置初始化

我们在Player类中添加设置位置的方法

void set_position(float x, float y)
{
    position.x = x, position.y = y;
}

并在GameScene::on_enter()中设置两个player的位置
player_1->set_position(200, 50);
player_2->set_position(975, 50);

运行程序可以发现两个角色能够正确显示,但是动画却卡住了。这时因为我们定义的两个子类重写了on_update()方法。我们可以在子类的on_update方法中调用父类的on_update()方法,像下面这样

Player::on_update(delta);

之后,动画卡住的问题成功被解决了。

评论

如果你已登录 GitHub,就可以直接在这里评论。