ID:41035
 
Keywords: demo, interface
Designing user-prompt systems for your game can sometimes seem daunting. You might want menus that branch into other menus and still more menus, without a lot of overhead. You want the system to be flexible and re-usable, so that you don't have to rewrite it for every prompt in your game, or for every game in your production. Sometimes, you don't even know where to begin. If this is the case, you're in the right place!

The "Are We There Yet?" Pitfall
More often than not, user-created prompt systems are programmed such that the server is continuously polling for a response. If the description alone doesn't register in your mind, here's an example:
client
var/waiting = FALSE

verb/Wait()
if(!waiting)
waiting = TRUE

src << "Press the NORTH key."

while(waiting) // Stop the verb until waiting is FALSE
sleep(5)

src << "You pressed NORTH!"

North()
if(waiting)
waiting = FALSE // End the waiting session

else
return ..()


This Wait() verb enters a while() loop which doesn't exit until the waiting variable is FALSE, which happens when North() is pressed. Sure, a prompt system could be built on this type of framework, as it does wait for user input before continuing, but it has several drawbacks:
  • It is continuously polling the waiting variable, eating up valuable system resources. Multiply this by, say, 50 users at a time and we're in a mess!
  • The response is being checked for once every 5 ticks. If the user responds at the beginning of one of these cycles, they have to wait to see the game acknowledge it, which is uncool.
  • It's just not a very flexible method for input.


A very good analogy for this system is the ever-comedic skit:

Passenger: "Are we there yet?"
Driver: "No."
Passenger: "Are we there yet?"
Driver: "No!"
...

Of course, in this case instead of annoying the driver, we are annoying both the the server and anyone in it. Instead, you want to change the system to work like so:

[Arrive at destination]
Driver: "We're there!"
Passenger: "Thanks!"

Creating Our Driver
Before we begin, we should establish how we are going to distribute the various roles in our system. This should be a good start:

Passenger - an object handles what happens once the ride's over
Driver - a datum simply responsible for knowing what's going on and telling the passenger that the ride's over.
Map - the user that directs the driver where to go

We want a driver that is very flexible. It shouldn't only be capable of going to one location (metaphorically speaking), or else we'd need a new driver for every path we want to take. Also, it shouldn't do anything more than what it needs to do; it simply asks the map (the user) where to go, and once the map responds we are there! It then informs the passenger, which does its own thing from there.

The driver doesn't need to know anything about the passenger. In fact, all it needs to know is what to ask the map, the possible answers the map can give, and who to relay the end destination (the user's response) to. So, for starters, here is our driver datum:
driver
var
passenger

question
list/choices

New(question, list/choices, passenger)
src.question = question
src.choices = choices
src.passenger = passenger

At this point, for clarity, I'm going to break from our analogy of drivers and passengers, and settle for what we're really dealing with. We're building a prompt system, and this driver object is the prompt. The passenger is the object that the user's response is relayed to. Here's the above snippet with more meaningful names:
prompt
var
responseObj

question
list/choices

New(question, list/choices, responseObj)
src.question = question
src.choices = choices
src.responseObj = responseObj


Great! Now we can build a new prompt simply by creating a new /prompt object and setting the response object (responseObj), the question to ask the user, and the choices to give the user. It's very flexible, since it makes no assumptions about what the question or answers are, or what to do with the answer once it's received. But what do we do with the prompt next? Well, we need to display it to the user. This is simple, give the user a proc that reads in the data from the /prompt object and displays it:
mob/proc/DisplayPrompt(prompt/P)
if(P)
var/answer = input(src, P.question) in P.choices

// TODO: tell the prompt we have an answer

There's nothing special about this prompt method at all; it uses BYOND's standard prompting function to get input from the user. The demo files found at the end of this article will give you some ideas for other ways you might display the prompt information.

After you get the answer from the user, you should feed it back to the /prompt object, and the prompt should then tell its responseObj what the response was. Before we send the answer to the /prompt object, we need to give it some way to receive and send out the user's response:
prompt
proc
PromptAnswered(selection, respondant)
if(selection in choices) // Make sure "selection" is a valid choice
if(hascall(responseObj, "PromptResponse")) // Make sure that "responseObj" has a handler function
call(responseObj, "PromptResponse")(src, selection, respondant)
return TRUE
else return FALSE

Now we can call prompt.PromptAnswered(answer, person_who_gave_the_answer). It will then verify that the answer is acceptable, and then check to see if its responseObj has a "PromptResponse" function to handle it---we aren't making any assumptions about the type of object responseObj is, except that it should have a proc for handling our user's answer called PromptResponse(), which takes 3 arguments (the /prompt object, the answer, and the object giving the answer). The proc returns TRUE (defined as 1) if the answer is valid, indicating that it was handled if it could be handled, and FALSE (defined as 0) if it was an invalid answer.

So now that we can handle the user's response, we should modify our previous DisplayPrompt() proc like so:
mob/proc/DisplayPrompt(prompt/P)
if(P)
var/answer = input(src, P.question) in P.choices

P.PromptAnswered(answer, src)


All that's left is to come up with the questions we want to ask, and the objects we want to handle them! Here's a full-blown example for you, using the user's mob itself as the responseObj:
prompt
var
responseObj

question
list/choices

New(question, list/choices, responseObj)
src.question = question
src.choices = choices
src.responseObj = responseObj

proc/PromptAnswered(selection, respondant)
if(selection in choices)
if(hascall(responseObj, "PromptResponse"))
call(responseObj, "PromptResponse")(src, selection, respondant)
return TRUE
else return FALSE

mob
proc
DisplayPrompt(prompt/P)
if(P)
var/answer = input(src, P.question) in P.choices

P.PromptAnswered(answer, src)

PromptResponse(prompt/P, selection, respondant)
if(respondant != src) // Make sure that the person answering is myself
alert(respondant, "I wasn't talking to you!")

else
if(selection == "Good")
alert(src, "That's great!")
else
alert(src, "Hope things work out!")

verb/My_Mood()
var/prompt/moodPrompt = new("How are you feeling?", list("Good", "Bad"), src)

DisplayPrompt(moodPrompt)


And on that note, I'll leave you with an extended version of the demo here.

Key Differences With the Demo
Now, suppose we want our response object to handle multiple prompts. How does it differentiate between them? Well sure, we receive the /prompt object as an argument to PromptResponse(), and we could always check against its question variable, but then any time we wanted to reword the question, we have to check for the new string, and so on. For this, I introduced a new variable to prompts, id. This is a constant identifier for the purpose of the question being asked.

In the article, the prompt information was displayed using DM's standard input() proc, which cleans itself up once the response is provided. Likely, if you're using a custom method to display your prompt, you'll have to clean up the stuff you display at some point as well. The demo/demo.dm file in the above project defines a HidePrompt() proc for prompt cleanup, and it is called in /prompt/proc/PromptAnswered() just before the answer is given to the response object.

Some methods of display (including the input() proc used in the article) accept an optional caption to display in the window's titlebar, or the heading of whatever is being displayed. The demo files also give the /prompt type a variable for this.

Using the Demo
The demo was built so that all of the important information --- the response objects, the questions they ask, and the map they are displayed on --- were abstracted from the methods used for displaying the prompts and receiving the answers. There are actually two different demos packaged into this, contained in demo/demo1/ and demo/demo2/. You can switch which demo is compiled by changing the #define DEMO statement at the top of demo/demo.dm. Demo #2 requires Lummox JR.DmiFonts.