Advertisement

How to structure the program design of card effects for a Trading Card Game in Java?

Started by June 26, 2021 09:23 PM
11 comments, last by smashhit 3 years, 5 months ago

I coded up the cards into player Hand. I did the damage comparisons between player and villain cards. I also programmed the mouse cursor clicks.

I have no idea how to code up code effects without turning my Sprite class into having card effects data. The reason that can happen is because I am using a Stack that stores Sprite to represent the player hand of trading cards.

I also don't know scripting or do not have scripting background. How do I isolate card effects to be its own thing or how would you approach coding up card effects?

Not exactly sure what you're doing with Sprite, but couldn't you make a Card class containing the Sprite as well as an instance of the CardEffect class? Then put instances of Card in the Stack and have all data available.

Advertisement

Alberth said:

Not exactly sure what you're doing with Sprite, but couldn't you make a Card class containing the Sprite as well as an instance of the CardEffect class? Then put instances of Card in the Stack and have all data available.

So my Sprite has a Vector2D, Image, Rectangle, Movement State, attackPower data. Are you suggesting a Card Effect class that stores all the card effects of the game?

In digital trading/collectible card games, you have some kind of scripting description of the card effects along with triggers. Those descriptions (I modded Duels of the Planswalkers back in 2015) are stored in some kind of text format (like XML) and loaded at runtime. Then the script block is translated to the script engine when the card is loaded in the game and events are hooked.

As long as your game is not as simple as having just a few interaction templates, you can barely get good results without a scripting engine in the background. You must however know your cards already somewhere, so change your existing code to be able to provide a card ID and a per game unique instance ID to identify the card to the script. Then you can manage loading the script from file whenever a card of the provided ID is drawn and keep track of the card instance via the unique ID, for example when an effect requires the card to be discarded.

It is then up to you how you handle the visuals

In other words, cards (which, as Shaarigan explained, are an exceptionally complex scripting subsystem) must not depend on sprites or other graphical details; they represent information that might or might not be shown. On the other hand the graphical side of the game is welcome to depend on the card handling core.

For example (let's assume the game is Magic: the Gathering) the rendering loop could, among many other things, iterate over each object on the battlefield that is a card (tokens handled similarly but separately), fetch its sprite from an asset cache (luckily, the printed card never changes), ask about the card's type and other properties and what it is attached to (to compute a suitable position on the screen, e.g. lands at the bottom sorted by being basic and by name, then creatures sorted by increasing power and name), ask whether it's tapped (to draw the sprite sideways), ask whether it's phased out (to draw it with some special effect), ask what counters it has (to add appropriate overlays), and so on.

None of this display logic has any impact on card objects, except for making sure that needed information is available.

Omae Wa Mou Shindeiru

Shaarigan said:

In digital trading/collectible card games, you have some kind of scripting description of the card effects along with triggers. Those descriptions (I modded Duels of the Planswalkers back in 2015) are stored in some kind of text format (like XML) and loaded at runtime. Then the script block is translated to the script engine when the card is loaded in the game and events are hooked.

As long as your game is not as simple as having just a few interaction templates, you can barely get good results without a scripting engine in the background. You must however know your cards already somewhere, so change your existing code to be able to provide a card ID and a per game unique instance ID to identify the card to the script. Then you can manage loading the script from file whenever a card of the provided ID is drawn and keep track of the card instance via the unique ID, for example when an effect requires the card to be discarded.

It is then up to you how you handle the visuals

Is there a convention for naming giving card ID to cards (ie id1 for Trooper card, id2 for Agent card)? If I get 1,000 in my game, should I just put all the ID numbers in one text file, and the card effect logics are all stored in a separate file?

Advertisement

LorenzoGatti said:

In other words, cards (which, as Shaarigan explained, are an exceptionally complex scripting subsystem) must not depend on sprites or other graphical details; they represent information that might or might not be shown. On the other hand the graphical side of the game is welcome to depend on the card handling core.

For example (let's assume the game is Magic: the Gathering) the rendering loop could, among many other things, iterate over each object on the battlefield that is a card (tokens handled similarly but separately), fetch its sprite from an asset cache (luckily, the printed card never changes), ask about the card's type and other properties and what it is attached to (to compute a suitable position on the screen, e.g. lands at the bottom sorted by being basic and by name, then creatures sorted by increasing power and name), ask whether it's tapped (to draw the sprite sideways), ask whether it's phased out (to draw it with some special effect), ask what counters it has (to add appropriate overlays), and so on.

None of this display logic has any impact on card objects, except for making sure that needed information is available.

That makes more sense. Unfortunately, I'm making a fan made game that uses art sheets where the artwork and the card border are all together instead of separate. But I will bear in mind if I making a game with separated art assets

superherocat said:
Is there a convention for naming giving card ID to cards (ie id1 for Trooper card, id2 for Agent card)? If I get 1,000 in my game, should I just put all the ID numbers in one text file, and the card effect logics are all stored in a separate file?

The ID can be a name or anything, so there is no convention at all. This is a card file from the time when I modded Duels of the Planswalkers just for fun

<?xml version="1.0"?>
<CARD_V2 ExportVersion="1055">
  <FILENAME text="MENTOR_OF_THE_MEEK_378310" />
  <CARDNAME text="MENTOR_OF_THE_MEEK" />
  <TITLE>
    <LOCALISED_TEXT LanguageCode="en-US"><![CDATA[驯良名师]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[Mentor des humbles]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[Mentor de los mansos]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[Mentor der Sanften]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[Mentore degli Umili]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[弱(じゃく)者(しゃ)の師(し)]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[온순한 자들의 스승]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[Наставник Кротких]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[Mentor dos Humildes]]></LOCALISED_TEXT>
  </TITLE>
  <MULTIVERSEID value="378310" />
  <ARTID value="138444" />
  <ARTIST name="Jana Schirmer &amp; Johannes Voss" />
  <CASTING_COST cost="{2}{W}" />
  <FLAVOURTEXT>
    <LOCALISED_TEXT LanguageCode="en-US"><![CDATA[「这课堂没有过不过关的问题。 真正的考验会在第一次满月时到来。」]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[« Ici il n'y a pas d'échec ou de réussite. Votre véritable examen de passage, c'est la première pleine lune. »]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[“En estos salones no se aprueba o reprueba. El verdadero examen llega con la primera luna llena”.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[„In diesen Hallen gibt es kein Bestehen oder Scheitern. Deine wahre Prüfung kommt beim nächsten Vollmond.“]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[“In queste sale non c’è promosso o bocciato. La vostra vera prova avviene con la prima luna piena.”]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[「これらの広間では、成功も失敗もありません。真の試練は最初の満月と共に訪れます。」]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[“이 수업에선 합격과 낙제라는 것이 없다. 제군들의 진정한 시험은 보름달이 뜨는 날에 펼쳐질 실전이다.”]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[«В этих стенах нет понятий "успех" и "провал". Ваше истинное испытание начнется в первый день полнолуния».]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[“Nestes salões não há aprovação nem reprovação. Seu verdadeiro teste vem com a primeira lua cheia.”]]></LOCALISED_TEXT>
  </FLAVOURTEXT>
  <TYPE metaname="Creature" />
  <SUB_TYPE metaname="Human" />
  <SUB_TYPE metaname="Soldier" />
  <EXPANSION value="DPI" />
  <RARITY metaname="R" />
  <POWER value="2" />
  <TOUGHNESS value="2" />
  <TRIGGERED_ABILITY>
    <LOCALISED_TEXT LanguageCode="en-US"><![CDATA[每当另一个力量小于或等于2的生物在你的操控下进战场时,你可以支付{1}。 若你如此作,则抓一张牌。]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[À chaque fois qu’une autre créature avec une force inférieure ou égale à 2 arrive sur le champ de bataille sous votre contrôle, vous pouvez payer {1}. Si vous faites ainsi, piochez une carte.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[Siempre que otra criatura con fuerza de 2 o menos entre al campo de batalla bajo tu control, puedes pagar {1}. Si lo haces, roba una carta.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[Immer wenn eine andere Kreatur mit Stärke 2 oder weniger unter deiner Kontrolle ins Spiel kommt, kannst du {1} bezahlen. Falls du dies tust, ziehe eine Karte.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[Ogniqualvolta un’altra creatura con forza pari o inferiore a 2 entra nel campo di battaglia sotto il tuo controllo, puoi pagare {1}. Se lo fai, pesca una carta.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[他のパワーが2以下のクリーチャーが1体あなたのコントロール下で戦場に出るたび、あなたは{1}を支払ってもよい。そうしたなら、カードを1枚引く。]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[공격력이 2 이하인 다른 생물이 당신의 조종하에 전장에 들어올 때마다, 당신은 {1}를 지불할 수 있다. 그렇게 한다면, 카드 한 장을 뽑는다.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[Каждый раз, когда другое существо с силой 2 или меньше выходит на поле битвы под вашим контролем, вы можете заплатить {1}. Если вы это делаете, возьмите карту.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[Toda vez que outra criatura com poder menor ou igual a 2 entra no campo de batalha sob o seu controle, você pode pagar {1}. Se fizer isso, compre um card.]]></LOCALISED_TEXT>
    <TRIGGER value="ZONECHANGE_END" simple_qualifier="objectyoucontrol" to_zone="ZONE_BATTLEFIELD" from_zone="ZONE_ANY">
    if TriggerObject():GetCardType():Test(CARD_TYPE_CREATURE) and TriggerObject() ~= EffectSource() then
    	if TriggerObject():GetCurrentCharacteristics():Power_Get() &lt; 3 then
    		return true
    	else
    		return false
    	end
    else
    	return false
    end
    </TRIGGER>
    <RESOLUTION_TIME_ACTION>
    local effectController = EffectController()
    if effectController ~= nil then
    	if effectController:CanPayResourceCost(1) then
    		effectController:BeginNewMultipleChoice()   
    		effectController:AddMultipleChoiceAnswer( "CARD_QUERY_MENTOR_OF_THE_MEEK_PAY_1_AND_DRAW" )   
    		effectController:AddMultipleChoiceAnswer( "CARD_QUERY_MENTOR_OF_THE_MEEK_DO_NOT_DRAW_A_CARD" )   
    		effectController:AskMultipleChoiceQuestion( "CARD_QUERY_MENTOR_OF_THE_MEEK_QUESTION", EffectSource() )
    	end
    end
    </RESOLUTION_TIME_ACTION>
    <RESOLUTION_TIME_ACTION>
    local effectController = EffectController()
    if effectController:CanPayResourceCost(1) then
    	local result = effectController:GetMultipleChoiceResult()
    	if result == 0 then
    		EffectDC():Set_Int(1, 1)
    		effectController:PayResourceCost( 1 )
    	end 
    end
    </RESOLUTION_TIME_ACTION>
    <RESOLUTION_TIME_ACTION>
    if EffectDC():Get_Int(1) == 1 then
    	EffectController():DrawCards( 1, EffectSource() )
    end
    </RESOLUTION_TIME_ACTION>
    <AUTO_SKIP>
    if EffectController():CanPayManaCost("{1}") then
    	return false
    else
    	return true
    end
    </AUTO_SKIP>
  </TRIGGERED_ABILITY>
  <UTILITY_ABILITY resource_id="1" qualifier="Resource">
    <COST mana_cost="{1}" type="Mana" />
  </UTILITY_ABILITY>
  <DECKBUILDING_ACTIVATION_LEVEL />
  <DECKBUILDING_ACTIVATION_LEVEL base_score="0">
    <DECKBUILDING_SYNERGY_BONUS score_per_unit="50" resource_name="Token" />
  </DECKBUILDING_ACTIVATION_LEVEL>
  <SFX text="COMBAT_BLADE_LARGE_ATTACK" power_boundary_min="4" power_boundary_max="-1" />
  <SFX text="COMBAT_BLADE_SMALL_ATTACK" power_boundary_min="1" power_boundary_max="3" />
  <AI_BASE_SCORE score="600" zone="ZONE_BATTLEFIELD" />
  <QUERYTEXT tag="CARD_QUERY_MENTOR_OF_THE_MEEK_PAY_1_AND_DRAW">
    <LOCALISED_TEXT LanguageCode="en-US"><![CDATA[支付{1}并抓牌。]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[Payez {1} et piochez une carte.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[Paga {1} y roba una carta.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[{1} bezahlen und eine Karte.ziehen.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[Paga {1} e pesca una carta.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[{1}を支払ってカードを1枚引く。]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[{1}를 지불하고 카드 한 장을 뽑는다.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[Заплатить {1} и взять карту.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[Pagar {1} e comprar um card.]]></LOCALISED_TEXT>
  </QUERYTEXT>
  <QUERYTEXT tag="CARD_QUERY_MENTOR_OF_THE_MEEK_DO_NOT_DRAW_A_CARD">
    <LOCALISED_TEXT LanguageCode="en-US"><![CDATA[不要抓牌。]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[Ne piochez pas une carte.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[No robar una carta.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[Keine Karte ziehen.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[Non pescare una carta.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[カードを引かない。]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[카드 한 장을 뽑지 않는다.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[Не брать карту.]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[Não comprar um card.]]></LOCALISED_TEXT>
  </QUERYTEXT>
  <QUERYTEXT tag="CARD_QUERY_MENTOR_OF_THE_MEEK_QUESTION">
    <LOCALISED_TEXT LanguageCode="en-US"><![CDATA[你想支付{1}抓牌吗?]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="fr-FR"><![CDATA[Voulez-vous payer {1} pour piocher une carte ?]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="es-ES"><![CDATA[¿Quieres pagar {1} para robar una carta?]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="de-DE"><![CDATA[Möchtest du {1} bezahlen, um eine Karte zu ziehen?]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="it-IT"><![CDATA[Vuoi pagare {1} per pescare una carta?]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="jp-JA"><![CDATA[{1}を支払ってカードを1枚引きますか?]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ko-KR"><![CDATA[{1}를 지불하고 카드 한 장을 뽑으시겠습니까?]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="ru-RU"><![CDATA[Вы хотите заплатить {1}, чтобы взять карту?]]></LOCALISED_TEXT>
    <LOCALISED_TEXT LanguageCode="pt-BR"><![CDATA[Você gostaria de pagar {1} para comprar um card?]]></LOCALISED_TEXT>
  </QUERYTEXT>
  <EXPLAIN play_tag="HINT_CREATURE_ABILITY_WHY_PLAY" removal_tag="HINT_CREATURE_ABILITY_WHY_REMOVE" is_removal="false" />
</CARD_V2>

The card ID here is 378310, which is just the sequential order in which the card would has been printed and is set by Wizards of the Coast. You can set your card IDs however you want.

My suggestion if you haven't decided how the unique ID should be created, just FNV hash the name and get a (considered to be) unique 32bit integer value

superherocat said:

Unfortunately, I'm making a fan made game that uses art sheets where the artwork and the card border are all together instead of separate. But I will bear in mind if I making a game with separated art assets

At the abstraction level we've been discussing so far (cards are complex entities with even more complex scripting and they have no reason to depend on graphics) It doesn't matter at all.

You mentioned a “Sprite” class, but

  • where a sprite gets data from (a portion of a spritesheet or a whole image) is an implementation detail with no impact whatsoever on the cards (the "final product" is a texture buffer and texture coordinates referring to it); you probably need only to separate images, which can be composite spreadsheets and probably correspond to asset files, and portions of an image that can be drawn as sprites.
  • it might be a good idea to represent GPU-side textures and image files as further related classes (they don't necessarily match the “logical” images and they have a different lifetime)
  • what a sprite contains (illustrations, graphical ornaments, card frames, solid borders, transparent text…) and how transparent sprites are layered are rather flexible graphic design choices
  • you don't need to draw everything with sprites (for example, solid borders could be simple polygonal geometry and text could be rendered on the fly from fonts), and certainly you won't draw just one sprite per card

Omae Wa Mou Shindeiru

You're getting solid answers. It's important to remember that visual presentation and data logic are independent from each other.

Scripting systems don't need to be complex, and they don't need to be heavily encoded in data. It could be relatively simple to have an enum for all your card attributes, and then have a simple integer in your card data that says which attributes it has. This card has attribute 4. That card has attribute 9. Another card has attributes 2, 5 and 6. Exactly what that means is up to the game.

They can be built up, and likely will be, composing that this card has attribute 4 with parameters 2 and 4, which in the realm of a card game might mean attribute 4 is a creature, 2 is power and 4 is toughness. Another attribute might be casting cost, which you might encode as 2RR. Another is that it uses graphic image “cardart19.dds", and has a specific block of text for display in the text area.

You can do more complex scripting, creating functions directly in the card's data as was described above, but simple options work as well for simple projects. Don't add complexity unless it is worth the extra cost.

This topic is closed to new replies.

Advertisement