Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

Question about Grid Based Movement

A topic by Gabriel Cornish created Feb 08, 2024 Views: 222 Replies: 9
Viewing posts 1 to 9
(+1)

How would I go about representing a player on a grid (say 5x5) and then display that as a simple map on a card?

  • From the map card: Players could click on a cell adjacent to them, which would take them to a specific card and update their position on the Map Card.

I made a quick mockup in Decker to describe what I mean. The pink dot would represent where the player is on the grid. They would be able to select the cell to the north or the east.

Thanks in advance for any ideas on a good approach to implementing this type of feature. 

  • Bonus points for any thoughts on how to keep track of already visited spaces.
Developer (1 edit) (+1)

There are a number of ways you might go about doing this, with varying degrees of complexity. Here's one approach:

I start by making my map background, a field widget (which can be made invisible) to store the player's coordinates on the map called "location", a small canvas that can serve as a repositionable colored dot called "indicator", and a grid of buttons whose name corresponds to their coordinates:

In the "view" event handler for the card, I'll reposition the indicator and then update the .show state for all the buttons: the neighbors of the player's location should be "solid" and the rest should be "none":

on view do
 indicator.pos:card.widgets[location.text].pos
 loc:0+"," split location.text
 neighbors:(list "%i,%i")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)
 each name in (list "%i,%i")format 4 cross 4
  card.widgets[name].toggle["solid" name in neighbors]
 end
end

Then you can give the buttons an appropriate script. (Note: you can select multiple widgets at once and use Widgets -> Script... to modify their scripts at the same time) For example, if the destination cards followed the same naming convention:

on click do
 location.text:me.name
 go[me.name]
end

As for tracking visited locations, the easiest approach might be to place an invisible checkbox widget on each location and use the view[] event on those cards to mark it visited. There's a similar question in this thread with more detail.

Does that point you in the right direction?

(+1)

Thank you so much for such a quick response.

This definitely points me in the right direction. I get the general gist and logic of the implementation, but now I realize the Lil syntax is something I need to get more familiar with.

It looks like the function:

  1. Moves the player token to the position of the button that has the matching name of the location field
  2.  Not sure what this is: "loc:0+"," split location.text" - updating the player location?
  3. Stores a list of neighbors
  4. Iterate through the list to toggle them as selectable?

I'll spend some time reading up on the Lil documentation, but this gives me a good direction on where to start. Thanks again!

Developer (1 edit)

1. Correct.

2. This expression makes a local variable called "loc" which is the integer cast (adding zero to a string is one way to reinterpret it as a number) of splitting apart the text contents of the location field on commas. Put another way, this turns a string in that field like "2,3" into the pair of numbers (2,3).

A different, maybe more general, way of doing this might be

loc:"%i,%i" parse location.text

3. Correct. This expression is a bit complicated, so looking at it one step at a time in the Listener might be clearer. Assuming "loc" is (2,3), step by step,


4. Mostly. The phrase

(list "%i,%i")format 4 cross 4

Computes the names of every button widget:

("0,0","1,0","2,0","3,0","0,1","1,1","2,1","3,1","0,2","1,2","2,2","3,2","0,3","1,3","2,3","3,3")

The "widget.toggle[string bool]" method sets the .show attribute of the widget to the string if the bool is truthy, and otherwise "none".

(+1)

Thanks for breaking this down. Going to play around and see if I can get it working.

Also, thanks for making Decker. I'm glad a tool like this exists. It's so cool.

I was able to get this work! 

As an exercise in trying to learn Decker, I tried to turn this system into a contraption. I got it to work in the Prototype editor, but as soon as I drop the contraption into a card, it seems to fall apart.

Any idea what I'm doing wrong?

%%WGT0{"w":[{"name":"Grid Map1","type":"contraption","size":[172,161],"pos":[170,91],"def":"Grid Map","widgets":{"0,0":{},"1,0":{},"2,0":{},"0,1":{},"1,1":{},"2,1":{},"0,2":{},"1,2":{},"2,2":{},"location":{"value":"2,1"},"player":{}}}],"d":{"Grid Map":{"name":"Grid Map","size":[172,161],"margin":[0,0,0,0],"description":"Selectable grid map. Clicking a box will navigate the player there and show available locations","image":"%%IMG2AKwAoQD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8AnwFBAGoBQwBpAUQAaQFDAGwBBAAbAQQAGQEEAGwBBAAbAQQAGQEEAGwBBAAbAQQAGQEEAGwBBAAbAQQAGQEEAGwBBAAbAQQAGQEEAGwBBAAbAQUAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBAAcAQQAGAEEAGwBBQAbAQQAGAEEAG0BBAAbAQQAGAEEAG0BBAAbAQQAGAEEAG0BBAAbAQQAGAEEAG0BBAAbAQQAGAEEAG0BBAAbAQQAGAEEAG0BBAAbAQQAGAEEAG0BBAAbAQQAGAEEAG0BBAAbAQQAGAEEAG0BPwBtAT8AbQE/AG0BPwBtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQQAGwEEABgBBABtAQUAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAQQAGgEEABgBBABuAT4AbgE+AG4BPgBvATwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8AhQ==","attributes":{"name":[],"label":[],"type":[]},"widgets":{"0,0":{"type":"button","size":[19,20],"pos":[16,16],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend","show":"none"},"1,0":{"type":"button","size":[19,20],"pos":[49,16],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend"},"2,0":{"type":"button","size":[19,20],"pos":[79,16],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend","show":"none"},"0,1":{"type":"button","size":[19,20],"pos":[16,50],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend"},"1,1":{"type":"button","size":[19,20],"pos":[49,50],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend","show":"none"},"2,1":{"type":"button","size":[19,20],"pos":[79,50],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend"},"0,2":{"type":"button","size":[19,20],"pos":[16,82],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend","show":"none"},"1,2":{"type":"button","size":[19,20],"pos":[49,82],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend"},"2,2":{"type":"button","size":[19,20],"pos":[79,82],"script":"on click do\n location.text:me.name\n player.pos:me.pos\n loc:0+\",\" split location.text\n neighbors:(list \"%i,%i\")format flip loc+(list -1,1,0,0),(list 0,0,-1,1)\n each name in (list \"%i,%i\")format 3 cross 3\n  card.widgets[name].toggle[\"solid\" name in neighbors]\n end\nend","show":"none"},"location":{"type":"field","size":[100,20],"pos":[64,128],"border":0,"value":"1,1"},"player":{"type":"canvas","size":[15,13],"pos":[49,50],"locked":1,"script":"on click pos do\n \nend\n\non drag pos do\n \nend\n\non release pos do\n \nend","border":0,"image":"%%IMG2AA8ADQASKAgABigKAAUoCwAEKAsABCgLAAQoCwAEKAsABCgLAAQoCwAFKAoABygHABI=","scale":1}}}}}
Developer

The problem is "card.widgets", I'm afraid.

A Card interface has a .widgets attribute, and a Prototype interface has a .widgets attribute, but a Contraption interface does not. While it is occasionally desirable to iterate over the widgets of a contraption instance (as in this case) from the inside, it is somewhat undesirable to expose the same information to code outside the contraption, since it would violate the principle of encapsulation.

Making something along these lines work in a contraption would require a somewhat different approach. Perhaps instead of a grid of buttons you could use a single Canvas widget, capture "click[]" events sent to it, and turn the neighbor-checking logic we used previously "inside-out". It would be a bit more code, but the advantage would be that you could design the system to work for a configurable, arbitrary-sized grid.

I think I understand what you're saying.

Could you clarify the approach? What do you mean by turning it "inside-out"?

Developer

In this new approach, I still have the location stashed in a field, I still have the indicator in a canvas (as a reference), and I have a single canvas for rendering the map and obtaining click events.

In the card's view event handler, I need to figure out the appropriate coordinates for drawing the indicator dot by centering it within some grid cell, based on the dimensions of the map canvas:

on view do
  cell:map.size/4
  loc:"%i,%i" parse location.text
  map.clear[]
  map.paste[indicator.copy[] (cell*loc)+(0.5*cell-indicator.size)]
end

Then in the canvas click event handler, I make sure that the cell the player clicked ("here") is exactly one cell away from the current location ("loc") by calculating the magnitude of their difference:

on click pos do
  cell:map.size/4
  loc :"%i,%i" parse location.text
  here:floor pos/cell
  if 1~mag loc-here
   location.text:"%i,%i" format here
   view[]
   # ... navigate to another card, etc...
  end
end

Thanks! I'll give it shot.