For convenience reason a card is split into multiple layers. The data layer which is what the databases manipulate. The visual layer which is a Godot Node2D to integrate into the scene system. The instance layer which helps with managing the card in-memory.
Data layer (CardData class)
The card data structure is designed to be simple and yet flexible. There is 3 types of data, categories, values and texts. It does not seem much but I think this enough to cover most of the use case.
Cards have the following properties:
id: Stringis the card's unique identifier inside the database
source_db: Stringis the database's unique identifier from where the card comes from
Categories help you define what is your card, is it an attack card? Or is it a defense card? Is it magical? Or is it a weapon? A category is composed of two parts, a meta-category and the category itself. The meta-category gives you the ability to type your categories to help create better classification. An example of meta-category would be "rarity". The category is the value given to a meta-category for a card. An example of categories, for the meta-category "rarity", would be "common", "uncommon", "rare" and "epic".
You can access the categories by using the function
CardData.get_category(meta_category). This function return the category as a string set for the given meta category, if the meta category does not exists for the card, it returns an empty string.
Values are a way to give numerical values to a card. Numerical value are usually at the center of the gameplay of many card games. As computer only understand numbers, the game logic manipulate them to determine what a card does. A value is composed of an ID and a numerical value. An example of value would be "mana", to define the mana cost of the card.
You can access the values by using the function
CardData.get_value(id). This function return the value as an integer for the given ID. To avoid error message if you are not sure what values your card has, check first with the function
Texts are a way to add textual information to a card. Text is not so central for the internal working of a card game but are very important for the player to understand what a card does. They are mostly a visual support for the two data type above. A text is composed of an ID and a textual value. And example of text would be "name", to define the display name of the card
You can access the texts by using the function
CardData.get_text(id). This function return the text as a string for the given ID. To avoid error message if you are not sure what texts your card has, check first with the function
To edit cards data, CardEngine provides a UI (see above). At the top you find the toolbox. The toolbox start with a field where you fill in the ID of the card. The ID is unique and the only required data needed to create and manipulate a card. Next the "Save to" button allows you to write the card in the selected database. If the card has been correctly written in the database, a success message is displayed. Following is the "Load from" button which allows you, when the ID is filled in, to read the card from the selected database. If the card cannot be read from the database, an error message will be displayed. Finally the combo box allows you to select the database on which you want to work on.
In the main area, there is 3 columns, each one correspond to a data type seen above. Each column behave the same way. At the top the list display the data already attached to the card, it is empty at first. Single click to select an item. Double-click to edit an item. At the bottom, the "Add" button allows you to create a new item, and the "Delete" button allows you remove the selected item.
Note: no modification to the card is save to the database until you press the "Save to" button.
Visual layer (AbstractCard scene)
The visual layer describes how the card data are displayed. To integrate best with the Godot scene system, a card's root node is a Node2D and the structure can be describe as follow:
- AnimContainer is a Node2D to which the animations are applied. The reason to have this node is to make animations independent of how the card is transformed by its container.
- Placeholder is a Node2D which is the root of what is displayed when the visual layer is not linked to the data layer (more details below)
- Front is a Node2D which is the root of what is displayed on the front of the card
- Back is a Node2D which is the root of what is displayed on the back of the card
- MouseArea is a Control which capture all the mouse interaction for the card
- TransiMerge is a Timer which keeps only the latest modification to the card made by its container in a given time
- EventMerge is a Timer which keeps only the relevant events resulting from player interaction in a given time
- Transitions is a Tween which animate transformation of the card by its container
- AnimPlayer is a Tween which play the animations defined by its container
Here is the process to create your card scene:
- Open the "Scene" menu at the top left of the Godot Editor and choose "New inherited scene"
- Look for
res://addons/cardengine/cardfolder and click the "Open" button
- Rename the root node to whatever you deem appropriate, like "NormalCard" and save the scene. You can save the scene where you want but for good practice save it as
- To be able to customize the behavior of your card, you need to have a script. To do so, right-click the root node and choose "Extend script" and save.
Once the scene is created you can start the customization process. You can place any Node2D or Control derivative nodes under the "Front" and "Back" node to create your visual. Common elements to the front and back can be placed directly under "AnimContainer".
An important step is to set the "Size" property to define the horizontal and vertical size in pixels of your card. It is important because containers need this information to calculate their layout.
Once your visual is ready there is a good chance that you want to display your data using it. To do so, connect the signal
instance_changed to your card script. You can then access the data with a call to
instance().data(). Here an example from the demo:
func _on_NormalCard_instance_changed() -> void: var data := instance().data() _card_id.text = data.id if data.has_text("name"): _name.text = data.get_text("name") if data.has_text("desc"): _desc.text = data.get_text("desc") if data.has_value("mana"): var val = data.get_value("mana") if val >= 0: _cost.text = "%d" % val else: _cost.text = "X" for child in _picture_group.get_children(): child.visible = false if data.has_meta_category("rarity"): if data.get_category("rarity") == "common": _common.visible = true elif data.get_category("rarity") == "uncommon": _uncommon.visible = true elif data.get_category("rarity") == "rare": _rare.visible = true elif data.get_category("rarity") == "mythic_rare": _mythic_rare.visible = true elif data.has_meta_category("class"): if data.get_category("class") == "basic_land": _basic_land.visible = true
Note on Placeholder
The visual layer comes with a placeholder which is meant to position cards in a container without having to attach an instance to it immediately. It is especially useful when dragging card around to show where they will be dropped. Usually you should not have to worry about it, placeholders are managed by the containers.
Instance layer (CardInstance class)
The instance layer is the last piece of this small puzzle. An instance is used to manipulate a card's data in-memory. There is 2 reasons for instances to exist:
- As seen previously, the card's ID is unique in the database, but when playing multiple copy of the same card can exist. Therefore we need an additional information to identify cards in-memory, in this case the reference, accessible using the
- As part of many card game gameplay, cards get modified over time, by other cards or other game mechanics. Instances allows you to store such modifiers for each card, as even copy of the same card can have different modifiers.
An instance store a reference to the data, that you can access using the