
Programming Games with Shiny || Dragon Realm || RStudio
00:00 Introduction 00:05 Fun dragon facts 00:35 Describing the Dragon Realm game 01:20 Outlining our approach 04:38 Coding the basics of our app 10:15 Programming our action buttons 14:10 A note on coding objects "outside" of Shiny 15:27 Programming cave choice logic 20:29 Connecting action buttons to our consequences function 29:40 Creating separate pages using tabsetPanel() 39:35 Conclusion You've most likely used Shiny to build a web app that displays data, but you can also use Shiny to build games! In this video series, Jesse and Barret pair program simply games in Shiny as a way to uncover and explore new features. And because we know you'll ask, Jesse is using the Woodland theme from the base16 palette. You can get it - and other themes - from the {rsthemes} package: https://github.com/gadenbuie/rsthemes Read up on tabset panels here: https://shiny.rstudio.com/reference/shiny/1.5.0/tabsetPanel.html Learn more about Shiny here: https://shiny.rstudio.com/ Got questions? The RStudio Community site is a great place to get assistance: https://community.rstudio.com/ Content: Barret Schloerke (@schloerke) and Jesse Mostipak (@kierisi) Animation, motion design, and editing: Jesse Mostipak (@kierisi) Intro music: RGift by Blue Dot Sessions (https://app.sessions.blue/browse/track/91282) Theme song: Hakodate Line by Blue Dot Sessions (https://app.sessions.blue/browse/track/91291)
image: thumbnail.jpg
Transcript#
This transcript was generated automatically and may contain errors.
Before we get started, would you like some fun dragon facts for kids? Yes, I would. So a dragon has zero to four legs, claws, scales, and possibly spikes. And it may also have wings. Sometimes they have horns and hair. A dragon can usually fly. Some dragons live in caves. And dragons hoard precious metals and jewels. Yeah, I like the last part. Gold.
Describing the Dragon Realm game
Are you ready to talk about dragons? I am. Okay. In this game, the player, or players, it's the two of us, we're in a land full of dragons. The dragons all live in caves with their large piles of collected treasure. Some dragons are friendly and share their treasure. Other dragons are hungry and eat anyone who enters their cave. The players approach two caves, one with a friendly dragon and the other with a hungry dragon. But we don't know which dragon is in which cave, and we have to choose between the two. So we're going to take all of this and put it into a Shiny app.
Outlining our approach
If we think about this, like in terms of running an R script, this wouldn't be the most complex thing. Like if I was just going to do dragon realm.r, right, this could, this is almost, oh gosh, this is what? Like an if-else statement? Right? Yeah, with a menu that you can ask the user for input.
But there's like, kind of like three big UIs that we need to look at, right? The intro UI, like, and choose a cave, there would be like, then the logic that we check and then ask to play again, I don't know, they can appear, not appear if I was to make a game.
The way that I start a Shiny app, I haven't programmed a Shiny app in a really long time. It's been a month. So I do Shiny app, and then I shift tab, and that, my friends, is the shortcut that I have to share with you. How do we go about this, Barrett? Like, I am very inclined to design a beautiful UI first, and I know that that is like what we saw with Winston and Wordle with Nick and Winston, like, should we always be designing the logic of our app first, or should we make it beautiful first? I am a fan of logic.
I really like that flowchart. So let's, I think we can turn that into like some comments here, and then we can try to fill in some functions. Okay, this is our flowchart. And so step one is the start, and I'm going to just be going back and forth between screens. So then introductory text, and then player chooses a cave.
All right, and then it, we check, we check for a hungry dragon, or a friendly dragon. I feel like this is a false dichotomy. Like you can be hungry, I mean, can you be hungry and friendly though? Maybe not. No, it's usually just hangry. Should bring him a Snickers.
Ah, all right, and then we ask to play again. And then we end. I mean, well, yeah, I mean, I guess if you play again, you could like go through as many times as you want, but ultimately, like, if you say no, then it's an end. Yeah, we'll do a line after 20 and say, if yes, go to start.
Coding the basics of our app
So where can we do logic that has nothing to do with Shiny at all? So logic that has nothing to do with Shiny, I guess what I'm thinking is introductory text, maybe? So this is an output. Can we do a text output? Yeah, that works for me. Just going to copy paste this introductory text.
Let's actually do one more thing above line five, and add, I just default to H1, and let's just say H1 intro. What stage are we at in this UI? Okay, so we'll do a comma because we're working with Shiny, and I am going to set this up to run in my viewer pane. And the text output doesn't need to be a Shiny text output. It's expecting an output matchup. So this is more like a paragraph, so just a p tag. Look at that. Boom. Game done.
Is there anything else we can do that would not be Shiny related? I don't think so, because any other text is now buried behind decisions. True. But we can also just kind of put everything in one big layout at the beginning, and we just, you know, we'll only update certain parts of the UI. So we could ask them the question then for which cave do you want to do, and maybe add into button.
So we could do, let's actually, let's do this, which cave would you like to enter? So which cave will you go into, one or two? And then we want two buttons. So we could do action button, input ID, and this will be, we can call it cave one. Yeah, we can do the label of cave one, please.
Okay. So this should give us, oh, and they're right next to each other too. I don't know why I expected them to stack, but perfect. Fun surprises. So let's try to separate the intro and the question.
So let's add another H1 above line 10. And probably because we're going from the intro to the question, we'll need like a continue button. Ooh, I like that. So you're saying here we'll do action button, input ID equals continue. And we'll do maybe like an intro continue, because it seems to be lots of transitions. That's a good point.
So then there's, I would say, what, an answer? Like a response? Consequences. All right. It's very dramatic. So this would be, this would be a text output of some kind, right? Because something's happening. But would we do it as a P for now? Yeah, we could have win text and lose text.
And then if we look at our flow charts, so that's this. And then we ask if they want to play again. Play again. And then that would be, I guess, another button. Or two. Input ID equals play again. And then we'll do one that is end game.
We'll switch the input IDs to be underscores. Because subsetting in R doesn't like minus signs.
Programming our action buttons
So what I feel like is there's a couple of directions we can go. One is we know when we play this game. We're not going to have the whole thing just laid out, right? We're going to see an intro and then the button is going to take us to this and then right. So there's different kind of UI pieces that show or like we break this UI up into pages. So that's one direction to go. Another would be the logic of like, oh, I click cave one, and then I see this consequence text.
Let's do some of the logic for, we can just print a message just to know that it's hooked up. But it doesn't actually have to do anything. We've got intro to intro underscore continue as our action button. In our new eyes. So now we're going to come all the way down to our server.
So we want side effects only. Do it. When the button is pressed. So observe event. Like a rough moment. My brain just forgot everything. Okay. So it wants the event. So with, is that just input intro underscore continue. Yes.
Okay. So observe event input, input, intro, continue. And then you said something really important that I do comma curly brackets. Because the action that we'll take. Yeah, bigger expression. So we want to make sure we have the curly bracket. And so since we're just hooking it up, I like message. Message. Continue button was pressed or something.
So if we just run this, what should happen is when I click the continue button, we get our message in our console. Perfect. So then we just kind of continue that. Or cave one cave to play again and no. Thank you. Correct.
So when we're talking about side effects, could you do a quick recap? What is a side effect in Shiny? Side effect is there should be no follow up reactivity. So there's nothing else to calculate. And the side effect would be like printing a message. Right. Because if we print to the console, like there's nothing that's going to happen after that. Correct.
Look at that. All of our buttons. Got it. Made a mistake. There we go. Play again. And game. Excellent. Buttons are hooked up.
And when we go through this app development, how to act personally, I leave messages until the very end. Because if you may have hooked it up now, but it may get unhooked. Due to something. And so then it's just nice to have. Especially since this is a very flow chart. So you're saying I could make mistakes in my code and unhook my buttons. Yes.
A note on coding objects "outside" of Shiny
Let's do a little bit more about the consequences. Let's yeah, let's fill in the dragon logic. Let's actually make this function outside of Shiny.
If I put something down here, does Shiny ever see it? Like if I write a function here, is Shiny going to be like, oh, there's that function. Or is this like scratch paper? So it will. Because it is outside the server function. The server function is only parsed. It is not executed immediately. It is only executed when the file is returned. The only thing that you need to do is 67 where it says Shiny app. That needs to be returned from the file. So that needs to be the last thing. That's the only requirement.
So actually the server function and the UI definition above is all free form. You can do anything you want in any order, as long as 67 is the last thing that's returned.
So actually the server function and the UI definition above is all free form. You can do anything you want in any order, as long as 67 is the last thing that's returned.
Programming cave choice logic
So we're going to develop a function. So, um, I mean, can we call it consequences? Or are we going to get ourselves in trouble with too much things? So we'll set up our function. And before we do it, we'll move 67 below our flow chart. So that it's the last thing returned.
Consequences. We are going to write a function. So the input, the argument to the function is going to be cave one or cave two. So we could say cave number.
There's, I mean, there's ways that we can actually like still down to a coin flip, but I like the idea of cave. Is this where we would set like friendly dragon, not friendly dragon. How would you define friendly dragon and code? So if we're doing a coin toss. Friendly dragon match or the not match. I guess I would say like, well, one, it theoretically doesn't matter. Right. Like we just have to make a decision. I would want the friendly dragon to be a match.
That idea goes really well. Because if we had three caves, you could have two angry dragons. Friendly dragon. I'm more thinking this of like a coin toss at a basketball game. Or a football game. Right. If you guess the toss, then, or you guess the random value, then. And so you could have like a six sided die. And if you guess the side of the die, you found the friendly dragon. Or a two sided die.
So would we just sample one or two? Yes. Sample. So sample is going to be, is it just one to two? Size equals one. Replace equals ball. I guess it doesn't actually matter. Correct. Because size is one.
And that's always, let's go ahead and kill the Shiny. So that you can run 59, five, six times. Oh, there we go. Okay. Lottery ticket.
So we're going to sample and then we want to assign this to something. Yeah, we could say friendly. Okay. So basically I want if cave number. I want my cave number. So that's whatever button they've pressed. So we can imagine that's the number one or two. So what I just do cave number equals friendly. Then if that's true, what would you do? So I guess it would be something like congratulations. You have a new dragon friend. What if it's not friendly? So then it would be else. Womp womp.
So that's the basic logic. So if the cave number equals, whatever we've said. So if we have a match, great, you've got a dragon friend. If not, you've been eaten. So this is fairly straightforward, but now we need to hook up consequences to our button press. Right before we do that, we test the function. Just to see that it works.
Same idea, but starting at 67. Thank you. All right. So then I could do like friendly one. Nope, that's not the name of the function. Oh, we got eaten. All right. We've got a new dragon friend. So it's picking up. Yeah, it looks like. Perfect.
Connecting action buttons to our consequences function
Okay, so now we have consequences. This returns the response text, so that is pretty cool. Can we scroll up to the UI definition to see the response text is a text output. We could call this consequence text, maybe.
So then we can observe event in cave one or two like we are doing and, but a little pitfall, are we about ready to do, I'm not going to put, I'm not putting a reactive inside my observer. Right. That's just an unfriendly dragon. It's like an unfriendly dragon that will climb out of your computer. We do not put reactives inside observers. Or renders or renders, but renders are. Yeah, yeah, yeah.
Yeah, so we don't do that. So we still need to render text somewhere. So where do we do that. Well, obviously it's outside the observer. So we can just start it. I like, since it's near cave one, cave two, logical cannot either. Again, let's just do it like after 55.
And this would be output. And then this has to match a really long name. It's descriptive. Alright, so I'll put consequence text. And that could we just do consequences function. So if we did that, there would be no reactive or reactivity. Like, you would not be able to click a button because consequences would only occur once.
So we need a render text to pair with the text output. Okay, so then can it be rendered text consequences. Closer warmer. So consequences since it's now inside the render text. But we haven't given it like a cave number. Yes.
I want the output of consequences text, and then I want it to print the concept I wanted to print either friendly or not friendly response. Based on cave number and cave number is going to be whether this button was pressed or this button was pressed.
If we were to write this in in normal R code, how would you not reactive code, would you use a variable in between. Yes. Yeah, I would say, like, cave number. Can I just kind of put that in where would I put my cave number. Let's, let's go ahead and put a cave number in there in 57.
Then pretend that it's reactive, and we'll then create it. Okay, so we have our cave number there. Right. Okay, so then, is it cave number, is it reactive reactive. There might be a more elegant solution by doing reactive, or something else, but I like the idea of doing reactive. It's very explicit, and you have very fine tuned control.
Okay, so then my reactive Val is, I guess where I'm still stuck is like I have two separate buttons and I don't know how to put which one was clicked inside my reactive. So if we were to do this in a normal, if statement, you'd be like, if button was pressed, then code, and you would say like one is. And then, else if button two is pressed, then two is. So, how do we assign to a reactive value, how do we assign new update the reactive Val.
Is there an update is, is there like an update function. Um, not yet. Um, so let's let's go ahead and go after line 50. Okay, because cave one was pressed in line 50 we know we're in that situation. So then we'll say cave under number parentheses one. So that's how you can update a reactive Val, you submit new number. So then this will be cave number two. Yes. And did that is this just going to work now. I hope so.
Um, so this is actually a consequences function. Um, but we, we can probably skip the consequences function because I like how it is, it's very independent of Shiny. We'll leave it as is. But let's update our render text on line 60.
And this is also why I like earlier we were trying to move in very small steps because we have a thing of length zero and we did like five different things. So which one was it. Yeah, so inside the render text. Um, the default, actually, what we can do in 59, let's do this one first. 59, the default value is null. Let's just pretend that they always picked cage, like cave one. So let's say the reactive valve one. Okay, so we would just do that. Yeah. And then let's run the app and see if that fixes our zero.
Okay, so stuff is happening in the console, but it's not happening there. So let's go check our function. So we've got a message, right? So our function prints out a message. So if we were running this independent of Shiny, our message is printing out to the console. We were doing this like in a standard R script that is behaving as we would expect. True. But the function needs to return the text, not display the text. So what if, can I just do return? You can do return. Since it's also the last thing in the function, you can just take off the return parentheses.
And it'll take, yeah, perfect. Ah, easier. So if we reload, okay. Great. And this is now kind of fun because the consequences is random. It's not like, oh, cave one is always friendly. If you refresh the app three, four times, then the consequences text should change. So you're very lucky. There we go.
So we got the consequences text to change. Oh, there we go. We've changed. It works. Cool. Great. But we're still not hooked up to our button.
Oh, so you should be able to hit cave one, cave two, and it should at least, you know, it may be the same text. So it's not like actually reading what I'm, oh, no, because it's recalculating each time. We haven't like linked it out. Recalculates each time. So each time I click this button, it is independently running sample.
So at some point, but you should only be able to guess once. And so that's where like intro should only be displayed. Then questions should only be displayed. Click the button once, then you go immediately to the consequences. You're not allowed to click the button. Got it.
So is this where we, so we've got kind of the logic plugged in. Is this where we start? So is this now where we would like chop everything up into four screens? Sounds good to me.
Creating separate pages using tabsetPanel()
You sent me a Slack message and my response was the what? You had said, okay. So you had said today is going to be all about tab set panel type equals in tab panel body with transitions via observe event and update tab set panel.
If you actually want to pull up the help for tab set panel in that quadrant there. Perfect. Great. And so let's scroll down and look at the examples. Sounds like we have help documentation. And what we're going to do is this second example, the UI one in the middle where we have a tab panel body.
And what this is, so tab set panel is you normally have like something that you can click on to change the different tab sets. If you do type is hidden, those things that you can click on disappear. Like the titles are gone. And yes, a little spooky, but the way you transition is using the update tab set panel function. So it's programmatic. So we know when the button is clicked, we programmatically change to the next. We know when something happens, we change to the next.
So it's not spooky, but we can do this as a tab panel and then transition to a tab panel body once it works. So I don't need to, I'm not doing a sidebar layout main panel, but I would pick up from like tab panel. Yes. So we can just tab panel, tab set panel there. Oh, tab set panel. Sorry. Thank you. And then all of this goes inside of it. Everything. Well, all of my UI. All of the UI except for Ford.
So we are now inside of a tab set panel. And let's now add in the ID for the tab set panel. So ID is going to be dragon tabs. Dragon tab. Yeah. And great. Then let's do, let's not do type is hidden. Maybe we write it out and then comment it.
So our first one is going to be, so am I going to put each chunk inside tab panel body? Let's do it as a tab panel first, but yes, the contents will be the second argument. Got it. Like tab panel. And then this will be intro. Yes. I like short, simple names here, even though they may not look pretty. Feeling a little attacked, but that's fine. No, no. I like what you did because if there's spaces, we can't update it programmatically as easily.
Intro question. And then I'm just kind of the way that I'm doing this, I'm just leaving my headings in here. I'm not going to worry about it too much. I'm going to remember to move my commas around. This is the tab panel. And this will be consequences. There are consequences when you choose consequences. This is your text because then you have to type it. And then if you want, you could actually put the play again in the consequences. Oh, that's true.
Is that everyone? I think so. I'm not getting any errors. So, okay. So I put everything in tab panel. If I run it. Cool. Yeah. So let's switch to the question. Switch to the consequences.
One, we would absolutely use this, like, in a report. Right? Like, we wouldn't necessarily, like, continue, like, but, like, data science, data analyst, business intelligence, right? Like, this is, oh, here's some intro text or instructions. And here's the, you know, okay. So this does relate to data science, I promise.
All right. So we've made panels. Now what do we do? So the update method, update tab set panel can be done. So it's programmatic updating. And that can be done with a regular tab panel body or a regular tab panel or a tab panel body. Required for tab panel body.
So let's go ahead and try to do this update tab set panel for all of our output or observed events. So one thing that I'm noticing in the help documentation is update tab set panel is actually happening in our server function. So it's not something we're applying into our UI. We're putting it into our server function. Correct. So what I'm seeing is it's in our server function. We have observed events. We have lots of observers. We've got a button pressed. Would I put update tab set panel around all of my messages? Just after.
I mean, you can have many lines of code in the observed events, like curly bracket section. That's why we do a curly bracket. So we can just paste it there. Like that? Mm-hmm. Oh, okay. But we need to fill out the details of where we're going to and how we're getting there. So first argument is it needs to be the session object. So I'll just do session comma because you're providing that on 55.
So now my second argument says hidden tabs. And that will match to the ID of the big tab set panel. And we said that was dragon tabs. Great. So we are professionals. And then selected, what is this? Well, what's the next argument, I guess is what I'm saying. Your destination. Where would you like to go? I would like to go to my cave section. So instead of intro, I would like to go to, can I just put in question? Yes, question. Oh, that's it. So like selected equals question? Yes.
All right. Let's see if the app works. Yeah, that was, I was like, we got to test this. Baby steps. You are in a land full of dragons. That's so cool.
So 59 can now be copy pasted kind of in a lot of different places. Boom. Yay. And then your, your console is freed up. Like everything. Look at that.
So now you said what you had some other things, other tricks. Oh, yeah. So let's turn this into hidden. Yeah. So type hidden. And we need to change tab panel body. So now it's tab panel to tab panel body. Whoa.
You know what we can do, you're going to come to our friend, the help documentation tab. And I'll body. Yep. And the signature for the function technically changed. So for a tab panel, the ID defaults to the title, which the title is the first argument. And I just had to provide simple names that could be interpreted as an ID. But a tab panel body, the first argument is the ID. So it switches a little bit, but like, just because we use my simple names and we didn't do anything else, it'll work out. It'll still work. Yeah. We don't have to change it. Right. Because if I had done like consequences of decisions, that's very hard to. That would be really bad for where we are right now.
Okay. Let's do that. So now I'm so excited. I think I know what's going to happen. So it took away our tabs. There's nothing. So now we can completely direct someone. Oh, my gosh. It works.
Conclusion
I feel like there's a lot of good things that we did and we learned here. I think this to me is such an interesting, like, obviously, making games is a ton of fun. I learned so much just doing this. But then I'm thinking, like, use cases for this. Like, this is a really, if you want to, like, direct someone through your app, this would be really useful. Or you can, like, direct them and give them choices. That's really powerful.
Force the workflow of your application. Like, when you go to intro, you can only go to the question. You cannot go to consequences. You have to go to question. You can only go to consequences.
Force the workflow of your application. Like, when you go to intro, you can only go to the question. You cannot go to consequences. You have to go to question. You can only go to consequences.
Barrett, this was so cool. It was so. I don't want to say it was easy. Because I still get stumped on reactivity. But, like, I thought this would be the hard part was hiding the tabs and, like, the conditional stuff. But that was ridiculous. That was so easy.
But you can also imagine if you didn't have the logic, the transition logic and the other things. Like, if we hid everything first, it's really hard to debug. Yeah. Like, how do you know if your consequences are working right now? Like, I don't know. Yeah. So just do the tab panels at the end. Lesson learned. Thank you for not letting me go down the path I tried to go down.

