Donnerstag, 8. August 2013

DialogScript: New Version 0.1.0

I just uploaded a new version of the DialogScript tool: https://dl.dropboxusercontent.com/u/10307034/bobscodingcorner/DialogScript_v0.1.0.zip

The language now features player choices and some smaller features like smart modifiers and wildcards. Everything is documented in the PDF file available in the archive.

I also implemented some examples which are available in the content assist feature, but I also provide them here, to give you an idea of how the new language design works:

#############################################################################

Example 1: Scout and Skull
#############################################################################
 
characters: Scout, Skull
 
// switches replace the former conditions and are either on or off (off by default!)
// I wanted to find a simple metaphor for boolean variables everyone can relate to
switches: talked_Skull is off, name, blinky_Nigel, skull_Nigel, pole,
skull_Rock, skull_Moved
 
// conditions allow to concatenate switches with 'and' operator
// this might seem like an unnecessary intermediate step compared to boolean logic,
// but I want to see if it proofs useful in practice to separate boolean logic that way
conditions
  HasTalkedSkull when talked_Skull is off
  HasTalkedPole when pole is off
  HasTalkedSkullRock when skull_Rock is off
  HasNotTalkedJake when name is off
  KnowsNameIsNigel when blinky_Nigel is on and skull_Nigel is off
  IsSkullMoved when skull_Moved is on
end
 
// scene names are now also in quotation marks, they are referable now
scene "Larger Scene Sample"
  // default lines now feature a 'defaults'...'end' structure
  // inside, you can also use conditional dialog, 
  // just as in 'first time' or 'parting' cases
  defaults
    Skull: "Hey!"
  end
 
  // the 'first time' and 'other times' conditionals are now merged together...
  // seems to me to be more homogenous 
  first time
    Skull: "What are you doing here?"
    Scout: "Huh!?"
  else
    Skull: "You again."
    // as mentioned, you can also nest conditionals here
    // they feature the more common and shorter 'if'-'else if'-'else' keywords now
    if (HasNotTalkedJake)
      Scout: "Hi Jake!"
    else if (KnowsNameIsNigel)
      Scout: "Nigel, how's it hangin'?"
    end 
  end
 
  // hubs are meant to be loops which contain player choices
  // hence, hub's need to be explicitly left using an 'exit'-statement or
  // by invoking another scene or hub (yes, hubs can be called :))
  hub "Main Hub"
    
    // by default, choices are offered to the player every time a hub (loop)
    // is executed. The modifier 'single' allows to mark a choice as 'only available once'.
    // Thought of it as nice abbreviation instead of creating a switch/condition pair for every 'single choice'
    // The name of the choice (e.g. "Talking Skull") can be used as a preview text for the player on screen.
    single choice "Talking Skull"
      Scout: "I never expected you to talk."
      Skull: "Why not? You seem to be doing remarkably well for a pussy-cat."
      Scout: "But you're just a skull."
      Skull: "So, you've got something against skulls have you?"
      Scout: "No, I..."
      Skull: "It's always the same - prejudice just follows me around."
      Scout: "Really?"
      Skull: "Well, not that I actually go anywhere..."
      Skull: "I blame Ray Harryhausen for stereotyping the whole subject of skeletons."
      switch on talked_Skull // at the end of a choice or block, you can change the value of switches and/or leave the hub
    end
 
    // Writing the examples proofed to me that it is useful to make choices optional
    // hence, inside hubs, you can use the special conditional statement 'choices if'
    // Note that it is not possible to nest these kind of choices to avoid too complex structures
    // but I'm sure that this isn't necessary
    // Conditions can be chained with the 'or' operator.
    // That way, you can create 'chunks' of booleans that need to apply together at the
    // 'conditions'-statement above, and chain them here as alternatives
    choices if (HasNotTalkedJake or KnowsNameIsNigel)
 
      choice "Name"
      // inside choices, you can use "standard" conditions (can also be nested just as outside of hubs)
        if (HasNotTalkedJake)
          Scout: "What's your name?"
          Skull: "I'm not in the habit of giving my name out to just anyone, you know."
          Scout: "It's something embarrassing, isn't it?"
          Skull: "Not at all! It's, er... Cutthroat Jake! Yes."
          Scout: "You just made that up."
          Skull: "No I didn’t!"
          Skull: "You can call me Jake if you like."
          switch on name
        else if (KnowsNameIsNigel)
          Scout: "That mouse over there..."
          Skull: "I hate that mouse.  Do you have any idea what he did to me?"
          Scout: "I..."
          Skull: "He had the audacity to build a nest in my eye socket."
          Skull: "I was only able to drive him out by whistling for six hours straight."
          Skull: "Do you realise how difficult that is when you don’t have any lips...?"
          Scout: "The mouse told me your real name is Nigel."
          Skull: "Well..."
          Scout: "What sort of a name is Nigel for a skull?"
          Skull: "I know, I know..."
          switch on skull_Nigel
        end
      end
    end
 
    single choice "Pole"
      if (IsSkullMoved)
        Scout: "Why were you stuck on that pole?"
        Skull: "I was placed there as a warning to intruders."
      else
        Scout: "Why are you stuck on that pole?"
        Skull: "I've been placed here as a warning to intruders."
      end
      Scout: "Intruders?"
      Skull: "People like yourself, who come here looking for the treasure hidden in Skull 
              Rock."
      Skull: "Oops!  Forget I ever said that."
      Scout: "Not very good at your job are you?"
      Skull: "You call this a job? It’s certainly not one I would have chosen myself, if I’d 
              had any say in the matter."
      Skull: "It's not as if I can just walk away and get another job."
      Skull: "When a skull doesn't even have the rest of his skeleton with him, career 
              options are severely limited."
      Scout: "Talk a lot, don't you?"
      Skull: "It's the sheer and utter boredom – I haven't had a decent conversation in 
              forty three years."
      Skull: "Listen, you won't tell anyone that I mentioned the treasure, will you?"
      switch on pole
       end
 
    single choice "Skull Rock"
      Scout: "What can you tell me about Skull Rock?"
      Skull: "Not very much, I'm afraid.  I’ve never even seen it."
      Scout: "Why, because you don’t have any eyes?"
      Skull: "No, I can see you just fine."
      Scout: "Weird, that..."
      Skull: "Ever since I was placed here I’ve been facing in the same direction."
      Skull: "And the local monkeys are really awful – telling me how beautiful the view is 
              and how wonderful the sun looks as it sets over the sea."
      Scout: "Now, don’t get upset."
      Skull: "I’d weep if I had any tear ducts..."
      switch on skull_Rock
    end
 
    choice "Exit"
      Scout: "I’ll be seeing you."
      Skull: "Okay, bye.  Don't forget to come back soon."
      exit scene // at least one of these 'jumps' is mandatory, since the hub could never be left otherwise
    end
  end
end scene
      
#############################################################################
Example 2: Risen
#############################################################################
 
characters: PROTAGONIST, JAN
switches: place, foodOrGold, safe, offer, harbourTownQuest, followJanToTheWest
 
conditions
  AskedAboutPlace when place is on
  AskedAboutFoodOrGold when foodOrGold is on
  AskedAboutSafety when safe is on
  TalkedOffer when offer is on 
end
 
scene "BANDIT OUTPOST"
  
  first time
    PROTAGONIST: "I found this sword."
    JAN: "Good! I hope you know how to use it."
  end
   
  hub "Main"
    
    single choice "place"
      PROTAGONIST: "What can you tell me about this place?"
      JAN: "What do want to know?"
      switch on place
    end
 
    single choice "girl"
      PROTAGONIST: "There’s a girl came ashore with me, she’s in an abandoned house to the 
                    South."
      JAN: "South... Must be the old ship wrecker's house."
      JAN: "I’ll head that way later."
      JAN: "If she’s there."
      JAN: "I’ll take her somewhere safe."
    end
 
    single choice "help"
      PROTAGONIST: "I could do with some help before I go. I’m injured."
      JAN: "Hmm, so you are."
      JAN: "There’s a water barrel in the corner. Take a good drink and freshen up. 
            You stink of sea water."
      JAN: "If you’re planning on getting hurt again, you should get your hands on a few  
            healing potions."
      PROTAGONIST: "Where can I find some?"
      JAN: "Well, they don’t grow on trees. You’ll have to find a trader for those. 
            There are a few traders still around."
    end
 
    choices if (AskedAboutPlace)
 
      single choice "FoodOrGold"
        PROTAGONIST: "Looks like I’m going to be stuck here for a while. Anywhere I can get 
                      some food or pick up some gold?"
        JAN: "Ha! Not round here at any rate!"
        JAN: "I just asked for this post so I can watch the storms. Amazing things when they 
              blow past."
        JAN: "Strange how they never come inland."
        JAN: "Two weeks ago I would have sent you straight to Harbour Town. But not anymore."
        PROTAGONIST: "Why not?"
        JAN: "It’s full of inquisition. Protecting those ruins and recruiting for the damn 
              white robed cult at the Monestary."
        JAN: "I’m with the Don myself. We’re the ones who’re still free. Living in a camp in 
              the middle of a swamp. But freedom’s freedom."
        JAN: "I’d head to the swamp, see if the Don’ll take you in, or try your luck in 
              Harbour Town. Just stay away from the Monestary."
        switch on foodOrGold
      end
 
      single choice "monestary"
        PROTAGONIST: "You mentioned a Monestary. What happens there?"
        JAN: "The last place you wanna be!"
        JAN: "It’s where the Inquisition train their recruits. Brain wash’em!"
        JAN: "Stay well away from there."
      end
    end
 
    choices if (AskedAboutFoodOrGold)
 
      single choice "Safe"
        PROTAGONIST: "So, is Harbour Town safe? What are the Inquisition doing there?"
        JAN: "Not much at the moment. There are still some of our boys in there. They might 
              help you out."
        JAN: "And that white robed lot probably won’t bother you too much."
        JAN: "I can show you the way, but believe me, you’d be better off at our camp."
        switch on safe
      end
 
      single choice "Offer"
        PROTAGONIST: "What’s the Don got to offer me?"
        JAN: "Meat, bear, a job? They might even train you as a fighter. You got a sword, 
              after all. Most they’ll let you have up at the Monestary is a staff! Just a 
              big stick! That’s not a real weapon!"
        switch on offer
      end
 
    end
 
    choices if (AskedAboutSafety)
      single choice "Harbour Town"
        PROTAGONIST: "Can you show me the way to Harbour Town?"
        JAN: "Sure. Follow me."
        switch on harbourTownQuest
        exit scene
      end
    end
 
    choices if (TalkedOffer)
      single choice "Camp"
        PROTAGONIST: "All right, show me the way to your camp."
        JAN: "Good man! Follow me."
        switch on followJanToTheWest
        exit scene
      end
    end
 
    choice "End"
       exit scene
    end
 end
end scene
      

#############################################################################
Example 3: Heavy Rain
#############################################################################
 
characters: LAUREN, SCOTT
switches: convince, compassionate, persist, trick, buy, disappearance, johnny, johnnysHome, suspect, inRoomCenter is on, offerCigarette, police, johnnysFather, leads
 
conditions
       UnlockPhase2A          when convince      is on and compassionate  is on
       UnlockPhase2B          when convince      is on and persist        is on
       UnlockPhase2C          when compassionate is on and persist        is on
       TriedToBuy             when buy           is on
       HasTalkedDisappearance when disappearance is on
       HasTalkedJohnny        when johnny        is on
       HasTalkedJohnnysHome   when johnnysHome   is on
       HasTalkedSuspect       when suspect       is on
       HasntOfferedCigarette  when inRoomCenter  is on and offerCigarette is off
       TimesUpA               when police        is on and johnnysFather  is on
       TimesUpB               when police        is on and leads          is on
       TimesUpC               when leads         is on and johnnysFather  is on
       TimesUpD               when leads         is on and johnnysFather  is on and police is on
end   
 
scene "LAUREN’S MOTEL ROOM. EVENING"
 
  first time
    SCOTT: "My name is Scott Shelby. I’m a private detective.
            The families of the victims of the Origami Killer
            asked me to investigate the murders. I came here
            just to ask you some questions about Johnny."
    LAUREN: "I already told the police all I know and have nothing
            to add. Leave me alone."
  end
             
  if (TriedToBuy)
    LAUREN: "Leave me alone."
    exit scene // exiting a scene is now possible
  end
 
  hub "Phase 1"
      
    single choice "Convince"
      SCOTT: "The killer is walking around free as we speak.
              He’ll kill again if he’s not arrested."
      LAUREN: "My Johnny’s dead, so what difference does it make?"
      switch on convince
    end
            
    single choice "Compassionate"
      SCOTT: "I understand, Lauren. I know what you’re going through."
      LAUREN: "Oh, yeah? You know what it feels like to find your own
              son’s body on a wasteland? I’m sorry, I don’t believe you
              have the slightest idea what I’m going through, Mr. Shelby."
      switch on compassionate
    end
            
    single choice "Persist"
      SCOTT: "There’ll be other victims if we don’t stop the killer.
              You have got to help me, Lauren. You may know something
              that can aid the investigation."
      LAUREN: "Help you? There’s nothing you can do! My son is dead!
              Do you hear me? He’s dead!"
      switch on persist
    end         
      
    choices if (UnlockPhase2A or UnlockPhase2B or UnlockPhase2C)
 
      choice "Trick"
        SCOTT: "If we don’t find the killer, there’ll be other mothers
                who find their son’s body on a deserted wasteland.
                But you’re right! Why should you care? It’s not your problem
                anymore, right?"
        [Long pause, Lauren will sit down on the bed before she continues]
        LAUREN: "What do you want to know?"
        switch on trick
        enter hub "Phase 2" // this instantly switches to hub "Phase 2"
      end
                   
      choice "Buy"
        SCOTT: "I bought ten minutes of your time, didn’t I? All I ask is
                that you use that time to answer some questions"
        LAUREN: "You want to pay me to tell you about my son, is that it?
                You can buy my body, Mr. Shelby, but my son is not for sale!
                Get out of here! GET THE FUCK OUT!"
        switch on buy
        exit scene // player made the wrong choice    
      end
    end
  end
               
  hub "Phase 2" 
 
    // random is a modifier for conditionals and choices and allows the author to mark them
    // as random. The exact implementation needs to be done by the dialog engine
    random if (TimesUpA or TimesUpB or TimesUpC) 
      LAUREN: "Time's up Mr. Shelby. I hope you got what you wanted. Now get out of here."
      exit scene
    end
            
    if (TimesUpD) 
      LAUREN: "Time's up Mr. Shelby. I hope you got what you wanted. Now get out of here."
      exit scene
    end
            
    single random if (HasntOfferedCigarette) [Random, when the player does not move towards the bed]
      LAUREN: "Do you want a cigarette?"
      SCOTT: "No thanks, I quit."
      LAUREN: "That's brave."
      switch on offerCigarette
    end
                   
    single choice "Disappearance"
      SCOTT: "How did your son disappear?"
      LAUREN: "He used to go play with the neighborhood kids after school.
               It was pouring down something awful that day. I’ll never forget it.
               All his friends came home 'round 3, all except him."
      switch on disappearance                
    end
            
    single choice "Johnny?"
      SCOTT: "Tell me about Johnny. What kind of kid was he?"
      LAUREN: "Johnny was really a good boy. Sometimes, he fought with other kids who called 
               me a… you know. In his own way, I think he understood what was going on."
      switch on johnny
    end
            
    single choice "Suspect"
      SCOTT: "Did you suspect anyone after he disappeared?"
      LAUREN: "I meet a lot of pretty shady characters in my line of work. Sure, I thought 
               of it at first. But it didn’t seem to make any sense. I don’t believe any of  
               my clients could have done that to my Johnny... and all those other kids?"
      switch on suspect
    end
            
    single choice "Johnny's home"
      SCOTT: "Did Johnny live with you?"
      LAUREN: "Yes. Of course I made sure he never met any of my clients. I wanted to stop, 
               you know... but we needed the money. I was trying to earn enough to get us 
               out of here."
      switch on johnnysHome                         
    end
                                       
    random choices if (HasTalkedDisappearance or HasTalkedJohnny or HasTalkedJohnnysHome or HasTalkedSuspect)
            
      single choice "Police"
        SCOTT: "When did you sound the alarm?"
        LAUREN: "About eight o’clock. I began to get worried, I went all around the  
                 neighborhood, I went to the wasteland where they liked to play, I went to 
                 see his friends. I called the cops about ten o’clock."
        switch on police
      end
                   
      single choice "Johnny's father"
        SCOTT: "Tell me about Johnny’s father."
        LAUREN: "A loser without a job who liked to beat me after a few drinks...
                 He left the day Johnny disappeared. I ain’t seen him since. Coward!
                 Good thing he left."
        switch on johnnysFather
      end
 
      single choice "Leads"
        SCOTT: "Do you know if they found anything on the wasteland? Any leads or witnesses?"
        LAUREN: "No. They said he must’ve run away and would probably end up coming back.  
                 His body was found five days later with an origami figure in his hand and 
                 an orchid on his chest."
        switch on leads
      end
    end
  end
end scene
       
#############################################################################
Example 4: Mass Effect 2 (a bit more tricky, but it still works ;))
#############################################################################
 
characters: EDI, SHEPARD, GRUNT
switches: openTank, gaveName, phase5, convinced, playerShoots, shooting, hasShot
 
conditions
  OpenTank when openTank is on
  KnowsName when gaveName is on
  Phase5 when phase5 is on
  GruntIsConvinced when convinced is on
  PlayerShoots when playerShoots is on
  EndsWithShooting when shooting is on
  DoneShooting when hasShot is on
end
 
scene "NORMANDY SR-2. PORT CARGO. OPENING THE TANK."
 
  defaults
    EDI: "The subject is stable, Shepard. Integration with onboard systems was seamless."
  end
 
  hub "Phase 1"
 
    choice "Keep it sealed."
      exit hub // this directly leads to the 'parting' section at the end of the script
    end
 
    choice "Open the tank"
      SHEPARD: "Stand by. I'm going to open the tank and let him out."
      EDI: "Cerberus protocol is very clear regarding untested alien technology."
      enter hub "Phase 2"
    end
 
    choice "Is he aware?"
      SHEPARD: "Can he see anything in there? Does he know where he is?"
      EDI: "Unlikely. Current neural patterns indicate minimum cognition. Barring ship-wide 
            power loss, the nutrients in the tank could sustain him for over a year."
    end
 
    choice "Is he dangerous?"
      SHEPARD: "Any idea how dangerous this guy is?"
      EDI: "He is a krogan, Shepard. If you are asking whether he is actively hostile, I 
            don't have the necessary data to answer. Okeer's technology could impart 
            data, not methods of thinking. The subject may know of his views, but would not 
            necessarily share them."
    end
 
    choice "Detect anything odd?"
      SHEPARD: "What can you tell me about this guy? Anything unusual?"
      EDI: "The subject is an exceptional example of the krogan species, with fully formed 
            primary, secondary, and tertiary organs, where applicable. No defects of any 
            kind, aside from the genetic markers of the genophage present in all krogan. I 
            cannot judge mental functioning."
    end
 
  end
 
  // hidden hubs won’t be accessed unless they are called explicitly
  hidden hub "Phase 2"
 
    single choice "Leave him where he is then"
      exit hub
    end
 
    single choice "He's to valuable to leave"
      SHEPARD: "He's either a powerful addition to the crew or a time bomb. I'd rather deal 
                with it now."
      switch on openTank
      exit hub
    end
 
    single choice "Do as I say, EDI"
      SHEPARD: "I won't be second guessed on my own ship, by my own ship. Do it."
      switch on openTank
      exit hub
    end
 
  end
 
  if (OpenTank)
    EDI: "Very well, Shepard. The controls are online. The switch - and consequences - are 
          yours."
    [Cutscene where the tank is opened, forwarding to next scene]
    enter scene "NORMANDY SR-2. PORT CARGO. RECRUITING GRUNT." // here, we invoke another scene
  end
 
  parting
    SHEPARD: "I'm leaving it the way it is. Make sure nobody tempers with it."
    EDI: "Of course, Shepard."
  end
 
end scene
 
 
scene "NORMANDY SR-2. PORT CARGO. RECRUITING GRUNT."
 
  first time [Grunt smashes Shepard against the wall and holds him there]
    // wildcards (in braces) can be used in dialog lines to set placeholders
    // which can be replaced by an appropriate dialog engine during runtime
    GRUNT: "Human. {Gender}. Before you die, I need a name."
  end
 
  // this hub and the subsequent dialog line show how you can model choices which are handled completely
  // identical in the game, without the need to write redundant dialog lines
  // (GRUNT will respond identically, no matter what the player chooses)
  hub "Phase 3"
 
    choice "You'll get more than that"
      SHEPARD: "I'm Commander Shepard, and I don't take threats lightly. I suggest you 
                relax."
      exit hub
    end
 
    choice "Of course"
      SHEPARD: "I'm Commander Shepard of the Normandy."
      exit hub
    end
  end
      
  GRUNT: "Not your name. Mine. I am trained. I know things, but the tank... Okeer couldn't 
          implant connection. His words are hollow. Warlord, legacy, grunt... grunt. 
          \"Grunt\" was among the last. It has no meaning. It'll do. I am Grunt."
 
  hub "Phase 4"
 
    // Quotation marks need to be escaped with a backslash
    choice "Why \"Grunt\"?"
      SHEPARD: "You wouldn't prefer \"Okeer\"? Or \"legacy\"?"
      GRUNT: "It's short. Matches the training in my blood. The other words are big things I 
              don't feel. Maybe they fit your mouth better. I feel nothing for Okeer's clan 
              or his enemies. I will do what I am bred to do... fight and determine the 
              strongest -- but his imprint has failed." 
      exit hub
    end
 
    choice "You want to die?"
      SHEPARD: "Why do you want me to try to kill you?"
      GRUNT: "Want? I do what I am meant to-- fight and reveal the strongest. Nothing in the 
              tank ever asked what I want. I feel nothing for Okeer's clan or his enemies. 
              That imprint failed. He has failed."
      exit hub
    end
            
  end
      
  GRUNT: "Without a reason that's mine, one fight is as good as any other. Might as well 
          start with you."
 
  hub "Phase 5"
 
    choice "Join my crew. We’ll find it" [Paragon choice]
      SHEPARD: "I have a strong ship and a strong crew, a strong clan. You'd make it 
                stronger."
      GRUNT: "If you're weak and choose weak enemies, I'll have to kill you."
      SHEPARD: "Our enemies are worthy. No doubt about that."
      switch on convinced
      exit hub
    end
 
    choice "You feel nothing for Okeer?"
      SHEPARD: "Is it that easy for Okeer's perfect krogan to abandon his mission?"
      GRUNT: "Okeer is just a voice in the tank. If his imprints are true, then he created 
              something stronger than him. So he's not worthy of me. And if his hatreds 
              aren't strong enough to compel me, they've failed, too. I feel nothing. I have 
              no connection."
    end
 
    choice "My command is your reason" [Renegade choice]
      SHEPARD: "I took you and I released you. Follow my command, and you'll have purpose."
      GRUNT: "Nothing in the tank imprints indicated humans could be so forceful. 
              You command as though you've earned it."
      SHEPARD: "My enemies threaten galaxies. Everyone on my ship has earned their place."
      switch on convinced
      exit hub
    end
 
    choice "Stand down or else"
      SHEPARD: "I'm offering you the chance to join my crew. Stand down. I won't ask again."
      GRUNT: "Asking marks you as unworthy. That's why you'll die first."
      if (PlayerShoots) [Player chooses to shoot Grunt in a QTE]
        SHEPARD: "You will stand down."
        switch on hasShot
        exit hub
      else
        enter hub "Phase 6-1"
      end
    end
 
    choice "I released you. You owe me"
      SHEPARD: "You should show more gratitude. Refusing an ally can bite you in the ass."
      GRUNT: "If I can brush an ally aside, what use are they? You're only worthy of dying 
              first."
      enter hub "Phase 6-1"
    end
 
  end
 
  hidden hub "Phase 6-1"
 
    choice "You leave me no choice"
      SHEPARD: "I gave you a chance. This is on you."
      switch on shooting
      exit hub
    end
 
    choice "I was being generous"
      SHEPARD: "Talking was the generous Plan A. Here's Plan B."
      switch on shooting
      exit hub
    end
  end
 
  if (GruntIsConvinced)
    GRUNT: "Hmm. Hmph! That's... acceptable. I'll fight for you."
    [camera reveals that Shepard has been pointing his gun at Grunt the whole time]
    SHEPARD: "I'm glad you saw reason."
    GRUNT: "Hmm? Ha! Offer one hand, but arm the other."
  else if (EndsWithShooting)
    [gets shot by Shepard several times, but shows no signs of weakness]
    GRUNT: "Huh?"
    switch on hasShot
  end
 
  if (DoneShooting)
    GRUNT: "You offer one hand but arm the other -- and don't hesitate. Maybe you are 
            worthy. You will give me strong enemies? A chance to find my own reason for the 
            skills in my blood? Very well. I will fight for you."
    SHEPARD: "Try that again and I won't be so patient."
  end
 
  parting
    GRUNT: "Wise, Shepard. If I find a clan, if I find what I... I want, I will be honored 
            to eventually pit them against you."
  end
end scene

Samstag, 20. Juli 2013

Xtext article @AltDevBlog

As mentioned, I wrote an article showing how you can create your own textual programming languages using Xtext.
http://www.altdevblogaday.com/2013/07/19/creating-powerful-languages-with-xtext/

The article also features two screencasts, one showing the very basic version of the dialog scripting language in action, one explaining the grammar defintion file we need to define in order to get the language. I also embedded them here, but you might want to check them in combination with the article ;)


Dialog script sample + rich editor
 
Grammar definition for the dialog scripting langunge used in the article @AltDevBlog


Enjoy reading and let me know @AltDevBlog what you think about it!

Best,
Robert

Sonntag, 7. Juli 2013

Dialog Scripting Tool

Hello? Is this thing on *test* *test* ... phew, quite some time has passed.
I created a small dialog scripting language for game writers with the magnificent Xtext framework. It's still work in progress, but I think I have reached a state decent enough to share. So feel free to download the thingy here:
https://dl.dropboxusercontent.com/u/10307034/adbad/DialogScriptDSL.zip

Yep, the archive contains an exe file, so your anti-virus might complain. Right now, it's only available for PC. You need a 32 bit JRE on your machine to run it. Other versions might follow.

Here are some screenshots to get you an idea what I'm talking about:

Simple, conditional dialog

Larger example scene

Context sensitive content assist feature

The language design is heavily based on a format suggested by Steve Ince in his book Writing for Video Games. Thanks Steve, allowing me to use this format as a basis and thanks for your valuable input so far!
For now, it's more of a small practice and showcase, but maybe it will grow to become attractive for the industr, you never know...

In order to getting started, I wrote a "How-to" you will find in the archive's root. It's also WIP.
I'm also working on a more decent article that shows how such a language can be created using Xtext. So, check for updates.

As always, any feedback appreciated.

Freitag, 25. November 2011

Hi,

since I'm now also writing stuff at AltDevBlogADay, it is hard to keep my CodingCorner busy. But maybe you will check out my articles (and all the others!) here:
Blog site: http://altdevblogaday.com/
My first post: http://altdevblogaday.com/2011/11/09/on-data-and-models-or-how-a-talk-by-jeff-ward-helped-me-get-back-on-track/
My second post: http://altdevblogaday.com/2011/11/25/with-great-powers/

Best,
Robert

Mittwoch, 3. August 2011

Dynamic Screen Management in XNA


[DOWNLOAD SOURCES + DEMO PROJECT]
[DOWNLOAD LIBRARY ONLY]

Welcome Back!

Today, I would like to talk about screen management in XNA. For a game I recently created, I was looking for a flexible yet stable screen management model in XNA. To keep a long story short, I ended up with an own solution which I would like to share with you.

Overview
I think it’s crucial that a game developer is absolutely free to decide which screen transitions should exist in their game and when they should be performed. However, screen management can be a daunting task and it’s tedious to reprogram it for every game. So my goal was to encapsulate the boilerplate part in a game library, making it reusable (for everyone), without limiting the mentioned flexibility. It’s up to you to decide how well I achieved this goal, I really appreciate every comment.
I present my solution in terms of a 2D example game to illustrate its flexibility, scalability and stability. I start with a description of the problem space to settle a common starting point. Then, I describe how you solve the task with the DynamicScreenManagement library. The third section deals with the internals of the library and how everything works. I end this article with some related questions on XNA I stumbled upon during the development of this lib. I’m really interested in what the community thinks of these points, so feel free to share your thoughts.

Academic Preface
Since I work as a lecturer, I’m always eager to find solutions which conform to OODP. XNA sometimes makes this a hard challenge. The reason I’m telling you this is that there surely are more direct solutions for screen management. However, I wanted to be sure to create a separation of concerns in terms of what a screen needs to know and what the game class needs to know. Let’s go to the problem space to make this more vivid.

Problem Space
Think of a 2D game where you have to explore a game world via a map. The map contains all locations the player can travel to in the game. When a player visits a map location, a new level starts (just like in Super Mario World). We can illustrate this as a screen graph. Figure 1 shows an example of such a screen graph.

Figure 1: Example Screen Graph
The game opens with the Start screen. There, we have one outgoing transition to the Map screen. It should be triggered when the player presses the A-Button. The Map screen has three outgoing and three incoming transitions. Depending on the state of the Map screen, pressing the A-Button should trigger either Level1, Minigame1, or end the game. The state in this example is the current location of the player character in the Map screen.
When a level or a mini game is finished, we want to return to the Map screen. Mini games return to the Map screen after a distinct amount of time (blue transition, tried to paint a stopwatch), while levels return after a distinct event happened (tried to paint a red bolt), for example after the player beats a final boss or dies.

Summing up the possible triggers of screen transitions, we have three kinds:

  • Input-Driven: The player interacts with the game and triggers a screen transition by purpose. For example, the player selects the A-Button on his controller to perform the transition from Start screen to Map screen.
  • Time-Driven: After a distinct amount of time has passed, the game triggers a screen transition by itself (end of a mini game).
  • State-Driven: The game reaches a former defined state (scripted event), which leads to a screen transition. For example, the player reaches a certain point in a level, or dies.
The three kinds of transitions can also be combined. For example, pressing the A-Button in the Map screen leads to a transition that also depends on the current state inside the Map screen. Certainly, time-based transitions could also be combined with screen states.

Despite the great demand on flexibility and thus arbitrary wiring of screens, the necessary interface of my library is very small and easy to use (as I hope :)).

Solution Space
Let’s dive right into a demo implementation using the library (the demo game complies with the structure of Figure 1), just to see how it works.


protected override void Initialize()
{
    /*** 1. Instantiate all your screens here ***/
    Screen startScreen = new StartScreen(this, "startScreen");
    Screen mapScreen = new MapScreen(this, "mapScreen");
    Screen miniGame1Screen = new MiniGameScreen(this, "miniGame1Screen");
    Screen level1Screen = new LevelScreen(this, "level1Screen");
    Screen gameOverScreen = new GameOverScreen(this, "gameOverScreen");
    Screen endScreen = new EndScreen(this, "endScreen");

    /*** 2. Instantiate the screen manager with your game's start screen ***/
    ScreenManager screenManager = new ScreenManager(startScreen);

    /*** 3. Tell the manager which transitions should exist ***/
    screenManager.AddScreenTransitionBetween(startScreen, mapScreen);
    screenManager.AddScreenTransitionBetween(mapScreen, miniGame1Screen);
    screenManager.AddScreenTransitionBetween(miniGame1Screen, mapScreen);
    screenManager.AddScreenTransitionBetween(mapScreen, level1Screen);
    screenManager.AddScreenTransitionBetween(mapScreen, endScreen);
    screenManager.AddScreenTransitionBetween(level1Screen, gameOverScreen);
    screenManager.AddScreenTransitionBetween(level1Screen, mapScreen);
    screenManager.AddScreenTransitionBetween(gameOverScreen, startScreen);
    screenManager.AddScreenTransitionBetween(gameOverScreen, endScreen);

    /*** 4. Tell XNA which screens exist (you don't need to use the
         XNA Components, but I think it's reasonable in this context) ***/
    this.Components.Add(startScreen);
    this.Components.Add(mapScreen);
    this.Components.Add(miniGame1Screen);
    this.Components.Add(level1Screen);
    this.Components.Add(gameOverScreen);
    this.Components.Add(endScreen);

    base.Initialize();
}

We see the Initialize method of the Game1 class of the demo game, realizing four steps.

  1. The first step is to instantiate our game screens. Note that all screen classes need to inherit from the Screen class of the DynamicScreenManagement library. We pass the game instance (this) and a screen-ID to the constructors, which I will come back to later.
  2. Second, we instantiate the ScreenManager class of the library. The constructor requires a reference to the start screen of the game.
  3. The third step is to add transitions between our screens, using the screen manager instance. Note that here we just tell which transitions exist, not on what they depend or how they are triggered.
  4. Last but not least, my Screen class is a DrawableGameComponent, so we can just add our screens to the XNA component list to let the framework call their client code.
Right now, we have told the library which screens exist, where the game starts, and which screens are connected directly. What misses is the individual state information when to trigger which transition. This can only be determined by the screens themselves.
public override void Initialize()
{
    this.WireScreenTransition(InputHandler.Buttons.A, true, "mapScreen");

    base.Initialize();
}

This is the Initialize method of the Start screen. We use the inherited method WireScreenTransition to determine which button (first parameter) and which screen state (second parameter) lead to which screen (last parameter). Here, we see what we need the screen-ID for. The Start screen does not hold a direct reference to the Map screen, so that we need to make use of an ID to wire the screens up properly.
In the example, we determine that the transition from Start screen to Map screen is defined by the A-Button and the screen state true.

public override void Update(GameTime gameTime)
{
    InputHandler state = InputHandler.Instance;

    if (state.IsKeyDown(InputHandler.Buttons.A, true))
    {
        this.PerformScreenTransition(InputHandler.Buttons.A, true, false);
    }

    base.Update(gameTime);
}

In the logic part of our Start screen, we determine that, when A is pressed, the game should perform the transition which is defined by the combination of the A-Button and the screen state true
. This already points to the fact that—for every screen—the combination of a pressed button and a distinct screen state must be a unique pair (the transition context), even if the button and/or the state does not trigger the transition at all.

To give two more examples, let's look at the implementation of the Map screen and then the Level screen:

public override void Initialize()
{
    /*** The player should start at the start location ***/
    this.currentMapLocation = new MapLocation("start", new Vector2(400, 10));

    /*** The transition to level 1 should be triggered when 
         A is pressed and the player is at the corresponding 
         map location at runtime ***/
    MapLocation level1MapLocation = new MapLocation("level1", new Vector2(400, 50));
    this.WireScreenTransition(InputHandler.Buttons.A, 
                              level1MapLocation, 
                              "level1Screen");

    /*** Same for the mini game 1 and end screen ***/
    MapLocation miniGame1MapLocation = new MapLocation("miniGame1", new Vector2(400, 90));
    this.WireScreenTransition(InputHandler.Buttons.A, 
                              miniGame1MapLocation, 
                              "miniGame1Screen");

    MapLocation endMapLocation = new MapLocation("end", new Vector2(400, 130));
    this.WireScreenTransition(InputHandler.Buttons.A, 
                              endMapLocation, 
                              "endScreen");

    ...
}

In the Map screen, the transition to another screen depends on the current map location of the player during runtime. Hence, we pass the locations which correspond to the given screens as second parameter when wiring the screens up. In fact, you can read the following line of code

...
this.WireScreenTransition(InputHandler.Buttons.A, 
                          endMapLocation, 
                          "endScreen");

just like: When "A" is pressed and the player is at the endMapLocation, the screen transition should lead to "endScreen". We perform the transition in the Update method of the class.

public override void Update(GameTime gameTime)
{
    InputHandler state = InputHandler.Instance;

    /*** Thanks to the specific screen wiring, the transition 
         performance is simple: When A is pressed, we pass the current 
         map location and the correct transition will be triggered.
    ***/
    if (state.IsKeyDown(InputHandler.Buttons.A, true))
    {
        this.PerformScreenTransition(InputHandler.Buttons.A, 
                                     this.currentMapLocation, 
                                     true);
    }
    ...

Here, we can use the runtime value of the currentMapLocation to trigger the correct transition when the A-Button is pressed. Easy, isn’t it?

Now, how does it work when the player input is not responsible for the performance of a transition? Very similar, as the Level screen shows:
public override void Initialize()
{
    /*** In this screen, we see how to wire screen transitions
         which are not triggered by user input. Apparently, the 
         first two parameters do not matter. Their combination just 
         needs to be unique for every transition from this screen.
         We just need to tell where the screen transition needs to point 
    ***/
    this.WireScreenTransition(InputHandler.Buttons.Empty, 
                              false, 
                              "gameOverScreen");
    this.WireScreenTransition(InputHandler.Buttons.Empty, 
                              true, 
                              "mapScreen");
    base.Initialize();
}
The wiring process of the Level screen looks a little bit different, using the same method. There is no user input that triggers the transitions this time. Nonetheless, we need to state a unique button--state pair (transition context) to define every transition. In the game logic, we use this context to perform the transition of choice.
public override void Update(GameTime gameTime)
{
    if (firstUpdate)
    {
        this.randomNumber = random.NextDouble();
        this.firstUpdate = false;
    }

    this.playerEnergy -= gameTime.ElapsedGameTime.TotalSeconds;

    if (randomNumber < 0.5f)
    {
        /*** When the player is dead, we perform the transition 
             to the gameOverScreen ***/
        if (this.playerEnergy <= 0)
            this.PerformScreenTransition(InputHandler.Buttons.Empty, 
                                         false, 
                                         false);
    }
    else
    {
        /*** in this case, we simulate a successful play and 
             return to the map screen ***/
        if (this.playerEnergy <= 1)
            this.PerformScreenTransition(InputHandler.Buttons.Empty, 
                                         true, 
                                         false);
    }
    base.Update(gameTime);
}
For the demo, I use a random number to determine how the level ends. Either, the player dies and we perform the transition to the Game Over screen, or the player beats the level and we return to the Map screen.

That's how it's applied. Now, let's have a look at the internals to see why it works this way.

The Internals
First, I would like to note that you can find the library (with all commented sources and the demo game) here [PROJECT + DEMO SOURCES DOWNLOAD].

The class structure of the library is illustrated in Figure 2. We see a very slim ScreenManager class. It knows the Start screen and offers the method to add transitions between screens. I also added a Reset method that iterates over the screen list and calls every ResetScreen method.

Figure 2: Class Diagram (click to enlarge)
The Screen class itself holds a SpriteBatch reference on which every screen should draw its content. To avoid one SpriteBatch instance for each screen, I use a private, internal SpriteBatchHolder class (not in Figure 2), which assures that every screen instance holds the same SpriteBatch reference. I don't like my current solution to achieve this "static" respectively "singelton-behavior" of the SpriteBatch instance, since it is not thread-safe and I'm not sure if it is even necessary to avoid several SpriteBatch instances for the sake of performance, but let's discuss this in the final section.

So, what's that ID stuff about? The problem is that a screen, when it wires up its transitions using the corresponding transition context objects (the method "WireScreenTransition", compare the Initialize methods of the screens above), it does not hold an instance of the target screen. Using an ID solves this problem, since we can use the transition context as a key to the corresponding screen ID, while we use the same screen ID as a key in another Dictonary to point to the corresponding screen. In the same time, this bears the issue that a game developer must be sure to use the same ID for a screen at two different locations. I illustrated this functionality in Figure 3.

Figure 3: How the wiring works (click to enlarge)
Discussion
Well, I'm very interested what the community thinks of my approach. There are several aspects I consider critical by myself, but in practice, it turned out that the library works fine,
especially when adding and wiring up new screens to a given game. What's more, you can playtest every screen very simple by adding the screen of choice as the start screen to the screen manager.
As an additional layer of abstraction, I think of an interface like realized in the SharpLudus project.

Furthermore, I would like to discuss some questions that might have come up during reading the text. At least, they came up while programming the library here ;)

Q: Why are the methods WireScreenTransition and PerformScreenTransition not overloaded, so that I would not have to state a button--state pair if it is not useful for my transition?
A: It would be possible to add overloaded versions, but this would bear several issues. The button--state pair defines a transition, even if it does not relate to it directly. Offering a overloaded version where the caller does not need to provide the context information results in a method that could only be called once. A design I don't like.

Q: What is that InputHandler class doing here? 

A: The InputHandler is a hangover of the game where I wrote the screen management for. You can rewrite the library very easy for "XNA-native" controller respectively keyboard input. I jus tlike the small InputHandler since I can play my game with both input devices and since it can distinguish a key down event as a single event if you use the overloaded method ;)

Q: Isn't it error-prone to use an ID at two different locations to wire the screens up?

 A: Yes, it is, but I don't have a better solution so far.

Q: Why did you built this stupid SpriteBatchHolder solution which is not thread-safe anyway?

A: My motivation is to draw on one and the same SpriteBatch object, since I suppose (and this might be rubbish), that there might be a performance issue using one SpriteBatch for every screen. Who knows more? It's not thread-safe, true, and it's not really a singleton, but since it is realized as a private inner class, there should not be a problem of multiple instances. One could add a locking mechanism if desired.

Q: So why didn't you just use the XNA Services to make the SpriteBatch available to everyone like described here?
A: I don't like the idea of global available references. My solution makes the SpriteBatch available to who should know about it, the screens, no one more, no one less.