Maciej Główka
Blog Games Contact

← Return to Blog Index

deck.png
April 22, 2023

Bevy roguelike tutorial / devlog part 6 - player cards

rust bevy gamedev

In the last part of the series we have created a possibility for the NPC units to attack the player. This time we are going to start building a card system, so the player can fight back :)

The source code for this part can be found at: https://github.com/maciekglowka/hike_deck/tree/part_06

Card trait

Cards in our game are going to be simple entities that will let the player execute various actions. Therefore, the most basic card mechanic should produce a valid Action object, which later can be injected into player's Actor component.

As we did with the actions, we are going to define this shared behaviour by creating a common trait. Let's start by adding it in a new cards.rs file of the player module:


// player/cards.rs
use bevy::prelude::*;

use crate::actions::{Action, models};
use crate::vectors::Vector2Int;

pub trait Card: Send + Sync {
    fn get_action(&self, owner: Entity, target: Option<Vector2Int>) -> Option<Box<dyn Action>>;
}

#[derive(Component)]
pub struct CardHolder(pub Box<dyn Card>);

The Card trait defines (for now) only a single method - that is going to construct the desired Action object. It requires two parameters: the player's entity and an optional target coordinate (eg. an attack or movement direction).

As we are going full ECS here, we are going to spawn the cards as standard Bevy entities. That's why I have also created a CardHolder component, that will store the assigned card logic.

So let's now define our initial cards for both walk and melee actions:


pub struct WalkCard;
impl Card for WalkCard {
    fn get_action(&self, owner: Entity, target: Option<Vector2Int>) -> Option<Box<dyn Action>> {
        Some(Box::new(
            models::WalkAction(owner, target?)
        ))
    }
}

pub struct MeleeCard(pub u32);
impl Card for MeleeCard {
    fn get_action(&self, owner: Entity, target: Option<Vector2Int>) -> Option<Box<dyn Action>> {
        Some(Box::new(
            models::MeleeHitAction{ attacker: owner, target: target?, damage: self.0}
        ))
    }
}

As you can see the implementation is pretty simple. Just take the parameters and build the expected action object. There is one important difference between the two structs though. The MeleeCard contains an extra numeric parameter, that defines it's damage value. This will allow us to make different attack cards (with different strengths) using the same base struct. Also in the future we can create an upgrade system that will bump this value up.

Now we can spawn some actual card entities. We are also going to create a resource to hold player's current deck:


#[derive(Default, Resource)]
pub struct Deck {
    pub cards: Vec<Entity>,
    pub current_card: Option<Entity>
}

#[derive(Component)]
pub struct Player;

fn spawn_player(
    mut commands: Commands
) {
    let walk_card = commands.spawn(
            cards::CardHolder(Box::new(cards::WalkCard))
        ).id();
    let melee_card = commands.spawn(
            cards::CardHolder(Box::new(cards::MeleeCard(1)))
        ).id();

    commands.insert_resource(
        Deck { cards: vec![walk_card, melee_card], ..Default::default() }
    );

    commands.spawn((
        Actor::default(),
        Health { value: 3 },
        Occupier,
        Player,
        Piece { kind: "Player".to_string() },
        Position { v: Vector2Int::new(0, 0) }
    ));
}

Deck logic

Now that we have our deck data all set up, we can implement some card operations. They will be controlled from the Input module, that ideally the Player should now nothing about. So, we are going to communicate the two with an event:


pub enum DeckEventKind {
    // emit from the input system to mark active card in the deck
    SelectCard(Entity),
    // emit from the input system to use the card with optional target coordinate
    UseCard(Option<Vector2Int>)
}

pub struct DeckEvent(pub DeckEventKind);

// mark that the player is ready to execute game action
pub struct PlayerActionEvent;

Apart from the DeckEvent we also define a PlayerActionEvent - it will be emitted by the Player module, after an action has been chosen and the turn update loop can start.

Now we can build two systems that would handle those events on the player's side:


// register systems and events in the plugin impl

app.add_event::<DeckEvent>()
    .add_event::<PlayerActionEvent>()
    .add_system(spawn_player.in_schedule(OnEnter(MainState::Game)))
    .add_system(dispatch_card.run_if(on_event::<DeckEvent>()))
    .add_system(select_card.run_if(on_event::<DeckEvent>()));

// cut

pub fn select_card(
    mut ev_deck: EventReader<DeckEvent>,
    mut deck: ResMut<Deck>
) {
    for ev in ev_deck.iter() {
        if let DeckEvent(DeckEventKind::SelectCard(entity)) = ev {
            deck.current_card = Some(*entity);
        }
    }
}

pub fn dispatch_card(
    mut ev_deck: EventReader<DeckEvent>,
    mut ev_action: EventWriter<PlayerActionEvent>,
    deck: Res<Deck>,
    mut player_query: Query<(Entity, &mut Actor), With<Player>>,
    card_query: Query<&cards::CardHolder>,
    mut queue: ResMut<ActorQueue>
) {
    for ev in ev_deck.iter() {
        if let DeckEvent(DeckEventKind::UseCard(v)) = ev {
            let Ok((entity, mut actor)) = player_query.get_single_mut() else { return };
            let Some(card_entity) = deck.current_card else { return };
            let Ok(card) = card_query.get(card_entity) else { continue };
            let Some(action) = card.0.get_action(entity, *v) else { continue };
            
            // action score does not matter for the player
            actor.0 = vec![(action, 0)];

            // the player moves first, so start with a single element queue
            queue.0 = VecDeque::from([entity]);
            ev_action.send(PlayerActionEvent);
        }
    }
}

The first, select_card system is solely responsible for setting the active card in the deck resource.

The dispatch_card system is slightly more complex - it prepares player's action for the execution. Once we get a valid UseCard event, we look for the player's Entity and Actor component. Then we try to get the current card from the deck and query it's components. If all goes well we can create the desired action (although this system doesn't even know what this action is!). The action object is then injected into the Actor component. Finally we let everybody interested know that the player is ready to start the turn.

Player input

In order to use the cards, we should change our Input module so it can control the deck. We do not have any UI at the moment, so we are going to implement a very basic, hard-coded card selection system for now.


// input/mod.rs
use bevy::prelude::*;

use crate::board::components::Position;
use crate::player::{Player, Deck, DeckEvent, DeckEventKind};
use crate::states::GameState;
use crate::vectors::Vector2Int;


pub struct InputPlugin;

impl Plugin for InputPlugin {
    fn build(&self, app: &mut App) {
        app.add_system(player_input.in_set(OnUpdate(GameState::PlayerInput)));
    }
}

const DIR_KEY_MAPPING: [(KeyCode, Vector2Int); 4] = [
    (KeyCode::W, Vector2Int::UP), (KeyCode::S, Vector2Int::DOWN),
    (KeyCode::A, Vector2Int::LEFT), (KeyCode::D, Vector2Int::RIGHT),
];

fn player_input(
    keys: ResMut<Input<KeyCode>>,
    mut player_query: Query<&Position, With<Player>>,
    deck: Res<Deck>,
    mut ev_deck: EventWriter<DeckEvent>,
) {
    let Ok(position) = player_query.get_single_mut() else { return };
    for (key, dir) in DIR_KEY_MAPPING {
        if !keys.just_pressed(key) { continue; }
        ev_deck.send(DeckEvent(
            DeckEventKind::UseCard(Some(position.v + dir))
        ));
    }

    // use this to temporarily switch between our only two cards
    if keys.just_pressed(KeyCode::Key1) {
        if let Some(entity) = deck.cards.get(0) {
            ev_deck.send(DeckEvent(
                DeckEventKind::SelectCard(*entity)
            ));
        }
    }
    if keys.just_pressed(KeyCode::Key2) {
        if let Some(entity) = deck.cards.get(1) {
            ev_deck.send(DeckEvent(
                DeckEventKind::SelectCard(*entity)
            ));
        }
    }
}

We are almost ready to test our player attacks. The only thing left is to switch the game state into TurnUpdate after we choose player's action. Up till now it has been controlled by the PlayerInputReadyEvent - but we have just dropped it. Instead we are going to use the aforementioned PlayerActionEvent. So let's import it into manager/mod.rs and change the appropriate system registration:


.add_system(turn_update_start.run_if(on_event::<PlayerActionEvent>()))

It is a small change, but it actually makes our game flow a bit more consistent. It is the actual game logic (inside of the Player module) that pushes the turn update forward - not some possibly random events from the input handling system.

We can now run the game and try to attack the NPCs when they reach us. Note though that at the beginning no card would be active - so you have to press 1 before you move (and 2 if you want to attack). Once we create some proper UI we are going to make this more clear, but now for testing it should be sufficient :)

← Bevy roguelike tutorial / devlog part 5 - more actions Bevy roguelike tutorial / devlog part 7 - better animation→