Author
Topic: Displaying an image on pause (Read 2926 times)

Hey all, sorry to kind of flood the board with questions. I've been saving them up for a year trying to figure them out myself first but there's a few I need help with.

So, I've been trying to learn Solarus and lua coding for a year and a half, and I still can't figure out how to create a pause menu. I've basically given up and accepted this as a design limitation to constrain myself with, so my game only has two items. But anyway, all my playtesters have given me the feedback that with all the side quests in my game, they'd really like a main quest log kind of feature because they forget what they were supposed to be doing. I think that just displaying the current stage of the main quest on the pause screen is the way to go.

So from studying the various solarus team scripts, I think that the way to show an image would be something like this:

However, I'm really confused about how all this works. It seems like you can create a surface, but it won't be immediately drawn? So you have to draw it, but you need a surface to draw it on. But wouldn't the second surface need to be drawn on a surface too? Ahh!

I'm okay with doing it the simple way at first, haha. I've looked through the code for ZS:DX, which is why I think the code I wrote in the first post should work, but it's not showing me anything. Is there a problem with that code?

From looking at the pause scripts in ZS:DX and others, it seems like this should work:

The command "pause_infra_img:draw(pause_surface)" draws that image only once (for 1 millisecond of maybe less).To show the image, you need to draw it at each iteration, and for that, you need to call it inside some "on_draw" event.Check scripts of finished projects to learn where that drawing code should be.

Logged

“If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you.”

Awesome! Thanks, Diarandor! I should probably get used to typing that, haha.

So for anyone interested in replicating what I've done (as it's like a baby step toward building your first pause menu, haha), here's how it goes. I'll try to do a little tutorial my past self would appreciate. My goal was, when you pause the game, a box comes up with the current step of the main quest. It's a little more complicated for me because there's a number of times when there's two main quest lines that you can do in either order, but anyway. Here's how it looks.

In my game_manager script, I've defined what to do when the game is paused. "pause_infra" is a separate script I've created that will work as a menu, which is a special way the engine can treat any table, from what I understand. So when the game is paused, I get that script using the require() syntax. Then, now that I have it, I'm going to call one of its functions, as well as start the menu via sol.menu.start()

After the game is unpaused, I want to stop the menu so it doesn't stay up. I've also got the choice to save, continue, or quit the game, which I learned from Christopho's wonderful tutorial on his youtube channel.

pause_infra = {} --this line means that we can refer to this a table called "pause_infra"local current_log_A -- these two lines are just setting up some local variables I'm going to be usinglocal current_log_B

--so if you remember in my game_manager script, I called pause_infra:get_game(game). I'm not 100% sure if I needed to type--"game" into the argument on both ends, maybe someone can correct me if I'm wrong. But the purpose of that is to give this--script the "game" userdata, just so it can call game:get_value(). This will allow us to display savegame values when we pause--the game, such as what our stats are, or in this case, what step of the main quest we're on.

function pause_infra:get_game(game)

--so what's going to happen here is I'm going to get the current step of the main quest from game:get_value("quest_log_a")--this value will be changed on each map where you complete a step. So for example, if you needed to defeat a monster,--when that happens, I'd change the value of "quest_log_a" in the monster's on_dead() event.--Then, I'm making a string variable called "current_log_A" which uses the number from "quest_log_a" to form the name of--a png file I have saved. So if you're on step 4 of the quest, current_log_A would equal "hud/quest_logs/quest_logA4.png"--Then this is repeated because I have two quest logs, an A and a B.

--Then this is where we actually display everything. The function pause_infra:on_draw() is automatically called by the engine--when the pause_infra menu is started (which I did in my game_manager script). You have to display images in the on_draw function--because otherwise, they'll only be drawn for 1 ms, which nobody will see. So now, every time the engine calles the :on_draw()--function (which seems to be constantly when the menu is running), the image will be displayed.--The way we do the image is to create a surface from a .PNG file, then just draw it.--I'm personally doing this for 3 things, 2 quest logs and a box around them. Later, I'll add in things like your attack or defense--stats, or an inventory, but this is just step one.

So yeah, thanks again, learning this has been super helpful, hopefully me documenting what I learned at a very fine level can help someone else trying to make a menu. I've still got a long way to go before I can make an item select menu, but this has been a good start!

I'm assuming "hud/quest_logs/quest_log_A"..templog_a..".png" is an image of a text string. You'll have more flexibility if you use text_surfaces instead, and it will make localization easier. What you can do instead is create entries in strings.dat to define all of your quest log strings. Here is an example of how I would do it.

local text_surface = sol.text_surface.create({--change these values as you see fitfont = "font_name",rendering_mode = "solid", --or "antialiasing", depends on font (use solid for bitmap font)color = {255, 255, 255}, --white, omit if using bitmap fontfont_size = 12, --omit if using bitmap font})--note that a size is not specified for a text surface, it will be as big as the text it contains

function pause_infra:get_game(game) --if it were me I would name this pause_infra:update(game), and likewise call it any time the contents to be displayed have changed local log_a_text_key = game:get_value("quest_log_a") --this string should match a key in strings.dat text_surface:set_text_key(log_a_text_key) --gets localized string in current language current_log_A:clear() --erase any previous content on surface text_surface:draw(current_log_A) --puts rendered text on surface, may need to handle cases where clipping occurs

local pause_img = sol.surface.create("hud/pause_infra.png") --move this function outside on_draw so you aren't destroying and re-creating this image a zillion times per second. You can do this because the image never changes

--draw quest log A --delete this line: local log_a_img = sol.surface.create(current_log_A) --the contents of this surface don't change while the pause menu is open, so only create this surface whenever the pause menu is opened current_log_A:draw(dst_surface, 100, 250) --specify coordinates for where you want to draw it on the screen

--draw quest log B --delete this line: local log_b_img = sol.surface.create(current_log_B) --the contents of this surface don't change while the pause menu is open, so only create this surface whenever the pause menu is opened current_log_B:draw(dst_surface, 100, 300) --places right below log_AendOn another note, you want to do as little stuff as possible in in the on_draw() function as possible since it gets updated so frequently (see my comments above). In your case, the contents of your pause menu won't change (unless you decide to make it interactive). So what you want to do instead is determine what content to display whenever the pause menu is opened, and create intermediate surfaces containing the corresponding images. No need to regenerate them on every call of on_draw().

Thanks llamazing! Yeah, the quest logs are images of text strings. I've gone and implemented your suggestions into my script, but it's given me a couple problems- the strings are cut off because they're too long and also I can only get the bottom half of the text to show. Here's what I did:

Only the bottom of the text is showing because the default vertical alignment is middle. setting the vertical_alignment = "top" somewhere between lines 11-16 of the code I posted will fix that problem. The alternate way to handle this problem is to specify a y offset at lines 24 & 29 of my code. You are drawing the text_surface at coordinates (0,0) of the intermediate surface. If the intermediate surface has a height of 32 and your font is the same, then you could do text_surface:draw(current_log_A, 0 ,16), for example. (0,16) is where you want the middle left point of the text to go using top and middle alignment. For left and top alignment you want the left top point to be at (0,0).

The second problem with the text being cut off is trickier, and I didn't consider the possibility of you wanting the objectives to wrap across multiple lines of text. Your options are the following:1) Fix where you want line breaks by adding "\n" in your string.dat text values. You will then have to split apart the text at the line breaks writing one line of text to the text_surface and intermediate surfaces at a time (and offset the lines appropriately when you specify the y coordinate to draw it at)2) You could auto-wrap the text by splitting the text to a new line once it exceeds your max width (this would be done in pause_infra:get_game()). Like before you'd have to write each line to the text_surface separately. Keep in mind with this second method that the way you determine the text width varies depending on whether you are using a bitmap or non-bitmap font.

I'm willing to help you with this if you provide your pause_infra.png image, the font you are using for the objectives, and a few representative text strings from strings.dat. It's hard to give an example without knowing some of the specifics of your implementation.

EDIT: please also specify your quest size and give the x,y coordinates of where you are drawing pause_infra.png

If you're willing to help, definitely! I've attached them to this post. The first one is the bitmap font I made for this, each letter is 7 pixels wide, just like the ALTTP font for convenience. I'm working in (what I think is default) 320x240 quest size. I made the pause_infra.png image the same size so that I could just draw it at 0,0. It's really just the box outlines.

Okay, great! Why don't you see if you can figure out how to do it, and I'll post how I would do it, then you can compare.

Here are some hints:No need for additional surfaces. In fact, you can do it with just one text_surface and one surface (i.e. current_log). Size current_log to be something like 134x72 judging by the pause_infra.png image.

Then specify a different y offset for each line of text when you call text_surface:draw(current_log) (first line at 0,0; second line at 0,16; etc. Adjust appropriately for more white-space between lines).

For determining where the line breaks occur with a bitmap font (assuming you don't do the \n method), you can simply count 19 characters (133 pixels) before starting the next line.

Once I find a way to get the part of my string after the line break, I'm pretty sure this will work. I've looked through the Solarus documentation quite a bit and I can't find any functions or anything to extract a line from a string (I've been working on this since you posted, haha). I'm currently looking through various Lua documentation and tutorials to figure this part out, any more hints as to where I'll find this sort of function?

Hint2: Use the function sol.language.get_string(log_a_text_key) to get a localized string that you can play with instead of setting it directly using text_surface:set_text_key(log_a_text_key). Once you've split the string to the substring you want to use, set the text of the text_surface with text_surface:set_text(my_substring)

local text_surface = sol.text_surface.create({ vertical_alignment = "top", horizontal_alignment = "center", font = "oceansfont",})--note that a size is not specified for a text surface, it will be as big as the text it contains

local log_a_text = game:get_value("quest_log_a") --the string saved to the game value "quest_log_a" should match a key in strings.dat local next_line = line_it(sol.language.get_string(log_a_text)) --gets localized string in current language

--draw quest log current_log:draw(dst_surface, 167, 27) --specify coordinates for where you want to draw it on the screenendI used center horizontal alignment because it looked better. I had to adjust the x-coordinates to compensate, see my comments.

I went the route of manual line breaks using \n. The disadvantage is you have to ensure none of your lines of text exceed 22 characters or it will get clipped. But it also gives you more control of choosing where the line break occurs. With the other method you might get a first line that is really long and a second line that is only one short word.

Also note that you can use the following notation in string.dat if you prefer. It might make it easier to tell if you've exceeded 22 characters on a given line. With this style, the line break is an actual line break rather than \n.

text{ key = "0", value = [[Find the chart salesmanin Ballast Harbor]]}For splitting the strings into lines I borrowed the line iterator from the ATTP dialog box script. Look up iterators if you are having trouble following how it works.

That works great! I switched the horizontal alignment back to "left" both as practice for learning how alignment affects the rest of the script and because it feels more like it's written in a journal when it's aligned left. But yeah, totally just personal preference. Here's what I did, which is basically what you have with tweaked values, and it works wonderfully.

local text_surface = sol.text_surface.create({ font = "oceansfont", vertical_alignment = "top", horizontal_alignment = "left",})--note that a size is not specified for a text surface, it will be as big as the text it contains

text = text:gsub("\r\n", "\n"):gsub("\r", "\n").."\n" --convert instances of \r to \n and add line break to end return text:gmatch("([^\n]*)\n") -- Each line including empty ones.end

function pause_infra:update_game(game)

current_log:clear()

local log_a_text = game:get_value("quest_log_a") --the string saved to the game value "quest_log_a" should match a key in strings.dat local next_line = line_it(sol.language.get_string(log_a_text)) --gets localized string in current language

text_surface:set_text(next_line()) --line 1 of quest_log_a text_surface:draw(current_log, 0, 0) --renders first line of quest_log_a text and draws on current_log surface

function pause_infra:on_draw(dst_surface) --draw menu architecture pause_img:draw(dst_surface) --draw quest log A current_log:draw(dst_surface, 166, 27) --specify coordinates for where you want to draw it on the screen

end

Now, I don't ever plan on using more than two lines for the quest log items, but for someone who might, I think you'd do, for example, 4 lines, like this? (starting at line 30)

So far far as the line_it function, I think I've got a grasp of it. The tricky line is

return text:gmatch("([^\n]*)\n")

From what I'm researching, this returns a pattern finding iterator, which (I've still got some research on iterators to fully understand them) will basically return some separate strings based on the pattern supplied to gmatch, like gmatch(pattern). The pattern you're using is encased in quotes, because patterns need to be strings, I think? Then you've got ([^\n]*)\n. The ^ symbol basically means, every character other than- in this case, since \n means line break, ^\n means everything other than line breaks. And this is in square brackets because the ^\n is a char-set you've created- but since you're char-set only contains one type (characters other than line breaks), is it necessary to make this its own char-set, or just a convention? Anyway, after that, you've got an asterisk, *, which is basically there to return empty lines, as it means no character will count as a character other than a line break. So all together, it basically means match and send back all characters other than line breaks that end with a line break, which since there's a line break added to the end of each line in the code above this, will send back individual lines.

This definitely requires a full scoop of lua knowledge, so I'm taking a lot out of this! Thanks so much, llamazing!

but since you're char-set only contains one type (characters other than line breaks), is it necessary to make this its own char-set, or just a convention?

It is necessary to use a char-set because the ^ character has a different meaning when used outside of the [] brackets (forces the match to be from the beginning of the string when used as the first character of the pattern).

Anyway, after that, you've got an asterisk, *, which is basically there to return empty lines, as it means no character will count as a character other than a line break. So all together, it basically means match and send back all characters other than line breaks that end with a line break, which since there's a line break added to the end of each line in the code above this, will send back individual lines.

Correct. Note that the asterisk also serves the purpose of capturing more than one consecutive instance of the [^\n] char-set and will match as many as possible. If you didn't want to return empty lines then you could change the * to +.