Handling cards in a digital card game

Hi everyone,

So as stated in my presentation, I am working on a very (very) simple prototype for a digital card game in the style of Hearthstone. At the moment, all I have is a single card which I can drag and drop on a rectangle standing for the board. If I drop the card outside the board, it goes back into its initial position. So not much, but it has been a good exercise. :grinning:

The first thing I would like to ask is how would you handle the cards. For now, I have in my main.collection a card Game Object but the game plays with a 45-cards deck, so should I create 45 Game Objects? That sounds overkill… Especially since the game should (eventually) be two players, so that would be 90 cards in total. Is there a more efficient way to handle this?

Thanks in advance for your help!

1 Like

There must be one game object (card). The card must have parameters that define its appearance (sprite, etc.) wia properties . Cards in the right amount are created by factories. Cards collision (battle) is determined by the trigger. :hugs:

5 Likes

Thanks a lot, that makes sense! I will try to see if I can make this work :smiley:

Yes, cards as game objects (or collections if you need more complexity in how the card visuals are created).

Maybe. You could also use a data driven approach where the card properties are stored in a Lua data structure and set by a central card handler script.

Yes, absolutely!

Again, maybe. It would require a script per card which isn’t strictly necessary (see note on properties vs central data structure).

It is very tempting to use properties and a script per card. It can feel good to think about things in an object oriented fashion. “The card is an object and I should therefore put the data and logic belonging to it in a script on the card.”

This is the way to do it from an OOP standpoint, but it doesn’t scale well. What if you have several thousand objects? You’d then have logic and data in a thousand places. You’d make the transition from the engine code to the Lua logic in each script a thousand times (expensive).

You could instead put the logic in Lua modules/functions, keep a list of all cards and their data in one place, and update the cards from a single script.

I’m not saying that one is superior to the other in all cases. You need to make a decision based on the game you are making. What are the rules and restrictions of your game? Will you only ever have a deck of 52 cards in play at any given time? Or will you have several hundred cards in play?

9 Likes

Hi, thanks a lot for your answer!

I am not sure to understand how to update the cards from a single script though. What I’ve come up with so far is that I have a JSON file referencing all cards in the game (only 2 for now). I have a factory.script with an init function fetching the json file then calling a generate_cards function (see code below).

-- factory.script
local function generate_cards(self, _, response)
  local data = json.decode(response.response)
  local p = go.get_position()
  local component = "/cardInstance#card_factory"

  -- looping over the cards
  for k, v in pairs(data) do
	print("Card name: " .. tostring(v.name))
	p.x = p.x + 200
	local go_id = factory.create(component, p, nil, { score = 10 }, .3)
	local card_url = msg.url(nil, go_id, "card_script")
	local card_width = go.get(card_url, "width")
	print(card_width)
  end
end

function init(self)
  http.request("http://localhost/mygame/cards.json", "GET", generate_cards)
end

This is working quite well, with the two cards being generated as Game Objects, with the correct name. But I don’t really now how to follow your suggestion of keeping all logic in one place. For now I have a card.script in the card.go Game Object but if I understand your reply correctly this might not be the most efficient way to go?

It will most likely work well in most cases. I’m merely offering another solution which will be more efficient in some situations where you have many objects. Here’s modified code which shows a bit of what I mean:

local function add_card(self, position, data)
	local component = "/cardInstance#card_factory"
	print("Card name: " .. tostring(data.name))
	local go_id = factory.create(component, position, nil, nil, .3)
	table.insert(self.cards, {
		id = go_id,
		data = data,
	})
end

local function remove_card(self, id)
	for i,card in ipairs(self.cards) do
		if card.id == id then
			go.delete(id)
			table.remove(self.cards, i)
			return
		end
	end
	error("Unable to find card!")
end

-- factory.script
local function generate_cards(self, _, response)
  -- store list of cards here
  self.cards = {}
  
  local data = json.decode(response.response)
  local p = go.get_position()

  -- looping over the cards
  for k, v in pairs(data) do
  	p.x = p.x + 200
	v.score = 10 -- why isn't the score in the data?
	add_card(self, p, v)
  end
end

local function do_stuff_with_cards(self)
	local total_score = 0
	for _,card in ipairs(self.cards) do
		go.animate(card.id, "euler.z", go.PLAYBACK_ONCE_FORWARD, 360, go.EASING_INOUT_QUAD, 1)
		total_score = total_score + card.score
	end
	print(total_score)
end

function init(self)
  http.request("http://localhost/mygame/cards.json", "GET", generate_cards)
end

Another example where you want this approach is in for instance a shoot’em up game where you may have hundreds of bullets. You don’t want a script on each bullet to handle bullet movement. You want a single bullet handler that moves the bullets. And in the simple case of moving bullets in a straight line you’d use go.animate() and maybe not even a bullet handler.

4 Likes

Ok so I’ve been tring to play around with your suggestion, but one thing I can’t seem to figure out is how to share data with the card Game Object created by the factory. For instance, giving the generated card a name, or a cost, which are values stored in the JSON file.

However I think about it, I always end up having the factory send a msg.post with the data to a card.script in the card Game Object. How could I have a single card handler in that scenario?

Thanks again for your help and time :slight_smile:

Why do you need to do this? I get a feeling that you are falling into the OOP trap again: “The card data needs to be on the card game object!”

Do you need it so that you can set a label on the game object with the name of the card?

You should use the game object id to look up the data. You could have a data structure where card data is keyed to game object id so that you’re able to do quick lookups.

2 Likes

OK I think I finally got it, thanks a lot!

I have another issue, which is quite weird. The data on my card (name, cost, etc.) are handled by labels which are set in my factory.script when the card is generated. However, the labels appear to be always on top and they overlap with other cards. :thinking:

I guess it is an issue with the z-position. I gave each label a z-position of 0.1 so they are over the sprite of the card which has a z-position of 0. Any idea why?

It’s due to the fact that the text is rendered last in the default.render_script.
You’d need to create a new material (a copy) of the original text material you currently use, then change the render predicate to tile, so that it is rendered/sorted with all the sprites (which also have a material with the predicate tile)

1 Like

I’m sorry but I can’t seem to make this work.
The tag in the default label.material is alreay tile so I’m not sure what to change :confused:

So I’ve made some changes and progress.

I’m not using labels anymore but rather a gui which I made stick with the card go by following the GUI follows GO example by @britzl. It’s working fine, but I am still facing the same issue : the GUI is rendered on top of everything else, causing some weird overlap between cards.

I created a custom card_gui.material with a card_gui tag, which I set correctly in the material property of my GUI. The weird thing is that material seems to be completely ignored…

I added in my custom.render_script a predicate in my init function:

self.card_gui_pred = render.predicate({"card_gui"})

And then the render.draw function in the update function:

render.draw(self.tile_pred)
render.draw(self.card_gui_pred)

But as I said, this does nothing and my GUI still seems to use the normal gui material. I tried to comment the render.draw(self.gui_pred) to be sure and it indeed made the gui disappeared.

Sorry about the long posts guys, but I’m really trying to understand! :confused:

I think @JCash accidentally led you on a wild goose chase. The label material uses the tile predicate and will automatically interleave labels with the sprites etc.

The problem is that you use the system font which actually renders separately on top of everything else. Create a new font and use that in your label and everything will be fine.

2 Likes

Well I had another issue with labels which made me switch to a GUI instead: I added a button to generate cards and after the fourth card I had an error message stating that I reached full label buffer.

If I were to stick with the GUI solution, do you know what my material issue might be?

Increase the max count in game.project: https://defold.com/manuals/project-settings/#max-count-4

Don’t. It will complicate things more than it is worth. Unless you create the entire card with GUI nodes instead (a viable solution actually).

1 Like

Ah, yes… my bad. Sorry about that.

Well I tried to create a new font and use it but I’m still getting the same problem. I tried creating a custom_font.material with a tile tag, but it also seems to be ignored…

No problem, it made me go learn about GUIs so it wasn’t for nothing :slight_smile:

1 Like

Can you share an example?

Sure, here are some screenshots.

First the card_labels.font, with a custom_font.material:

The custom_font.material, with a tile tag:

And finally the card.go, where the label effectively uses the card_labels.font:

Again, thanks a lot for your help!

1 Like

You don’t need a custom material. Use the normal label material.