Basic Yes/No Dialogue System

Hello everyone. :smiley: This is a basic dialogue system I wrote. Because it seems to function well (although truth be told, I don’t even fully understand how it does :thinking:), and there don’t seem to be many such things on this forum, I decided to share it here. I’m sure it could be much better, but hopefully someone will find it useful. :slightly_smiling_face:



It’s basically just two scripts and a framework:

This main script sits on the text that you want to display the dialogue. In addition to the listed sensor requirements, it also (probably) needs to be connected to an Always TRUE sensor.

import bge, textwrap

#Requires:
#  Sensors:
#    "ReceiveText": "Message" sensor, filtering on subject "Dialogue"
#    "TextAdvance": "Keyboard" or "Mouse" sensor
#  Actuators:
#    "Message" {Note, find a better name}: "Message" sensor, sending To self, Subject "Dialogue" 
#  Properties:
#    "wrapWidth": Integer
#    "wait": Boolean, default False
#    "endText": Boolean, default False

#--
def listToString(inList): #A different, more descriptive name is welcome
    """inList: A list of strings, which will be joined into one with newline characters between them"""
    if type(inList) is not list:
        raise TypeError("'inList' must be of type 'list'.")
    return str(chr(10)).join(inList)
#--
def splitListAtLength(firstList, length=3):
    """Arguments:
        firstList(list), the list to be split
        length(integer), the number of items after which to split FIRSTLIST. Defaults to 3.
    Returns:
        List(first LENGTH items of FIRSTLIST, the rest of FIRSTLIST)"""
    
    if type(firstList) is not list:
        raise TypeError("Variable 'firstList' must be a list.")
    
    nextList = []
    i = 0
    
    for a in range(len(firstList)):
        if i == length:
            try:
                item = firstList.pop(i)
                i -= 1
            except:
                item = None
                print("No more list!")
            finally:
                if item is not None:
                    nextList.append(item)
        
        i += 1
    
    return [firstList, nextList]
#--

cont = bge.logic.getCurrentController()
own = cont.owner

msgSens = cont.sensors['ReceiveText']
nextPage = []

if len(msgSens.bodies) > 0:
    for body in msgSens.bodies:        
        bodyLines = textwrap.wrap(body, own['wrapWidth'])
        bodyLines, nextPage = splitListAtLength(bodyLines)
        
        body = listToString(bodyLines)
        
        sender,p,body = body.partition("|") #Split the received message into sender, splitter, and body.  Splitter is useless.

        if body.endswith("Y/N"): #We need user input
            bge.logic.sendMessage(sender + "|PromptYesNo") #Send a message that will be used to bring up user interaction options
            body = body.rstrip("Y/N") #Remove the "Y/N" from the end of BODY
            a = 1
        
        if len(nextPage) > 0:
            nextPage[0] = sender + "|" + nextPage[0]
            cont.actuators['Message'].body = listToString(nextPage)
            own['wait'] = True
        else:
            try:
                type(a)
            except:
                own['endText'] = True
                    
        own.text = body    

if own['wait'] and cont.sensors['TextAdvance'].status == bge.logic.KX_SENSOR_JUST_ACTIVATED:
    cont.activate(cont.actuators['Message'])
    cont.deactivate(cont.actuators['Message'])
    own['wait'] = False
if own['endText'] and cont.sensors['TextAdvance'].status == bge.logic.KX_SENSOR_JUST_ACTIVATED:
    own.text = ""
    own['endText'] = False

This script sits on the Yes/No button objects (Yes, both of them). Again, it probably needs to be connected to an Always TRUE.

import bge

#Requires:
#  Sensors:
#    "GetSender": "Message" sensor, nonfiltering. Needed to get the entity speaking
#    "SenseMouseOver": "Mouse" sensor, listening for "Mouse Over"
#    "SenseMouseClick": "Mouse" sensor, listening for "Left Button"
#  Actuators:
#    "SendReply": "Message" actuator, subject "ReplyYesNo"
#    "HideObj": Any type; I used Edit Object>Replace Mesh to (attempt to) remove the buttons' collision
#  Properties:
#    "msgPayload": String, "ReplyYes" on the Yes button, "ReplyNo" on the No one

cont = bge.logic.getCurrentController()
own = cont.owner
objects = []

for scene in bge.logic.getSceneList():
    for object in scene.objects:
        objects.append(object.name)
        
for subject in cont.sensors['GetSender'].subjects:
    sender, sep, payload = subject.partition("|")
    
    if sender in objects: #Filter, since STR.partition may not split the string as desired
        cont.actuators['SendReply'].body = sender + "|" + own['msgPayload']
    
    if payload == "PromptYesNo":
        own.replaceMesh(own.name, True, True)
        own.reinstancePhysicsMesh(own.name, own.name)
    elif sender == "HideYesNo": #This is weird, but the 'HideYesNo' message doesn't follow the format of the other messages (sender|message), so when S.partition() happens, SENDER will have the payload.
        cont.activate(cont.actuators['HideObj'])
        cont.deactivate(cont.actuators['HideObj'])
        own.reinstancePhysicsMesh("NullMsh", "NullMsh")
    
if cont.sensors['SenseMouseOver'].status == bge.logic.KX_SENSOR_ACTIVE and cont.sensors['SenseMouseClick'].status == bge.logic.KX_SENSOR_JUST_ACTIVATED:
    cont.activate(cont.actuators['SendReply'])
    cont.deactivate(cont.actuators['SendReply'])

This is the dialogue object framework script. You’ll need to edit it appropriately before it’ll do anything noticeable. It is assigned (with the afore-mentioned changes) to every object capable of dialogue.

import bge, Util
#from DialogueLib import *Lines ##Uncomment and change this line as needed to import the correct list.
##                                Of course, you wouldn't /need/ to have the lines in a separate script.  I did this on a suggestion from a codeveloper, but you could just as well stick the list in this script.

#Requires:
#  Basic Dialogue-capable Object stuff.  Should I create a subclass of KX_GameObject that sets this all up automatically? Yes, I should. Am I going to? No, I'm not. Why? Because I have no idea how. :P
#  Sensors:
#    "MouseOver": "Mouse" sensor, set to event "Mouse Over"
#    "MouseClick_L": "Mouse" sensor, set to event "Left Button"
#    "PlayerInRange": "Near" sensor, looking for property "Player", Distance/Reset Distance 15
#    "ReadReply": "Message" sensor, filtering on subject "ReplyYesNo"
#  Properties:
#    "MsgPtr": "Integer" property

cont = bge.logic.getCurrentController()
own = cont.owner
if cont.sensors['PlayerInRange'].positive:
    if cont.sensors['MouseOver'].positive and cont.sensors['MouseClick_L'].positive:
        ##Again, uncomment and change the "*Lines" and the "DialogueText" as necessary
        #bge.logic.sendMessage("Dialogue", own.name+*Lines[own['MsgPtr']], "DialogueText")
    
if cont.sensors['ReadReply'].positive:
    #Worth mentioning here that this bit of code requires the body of the reply message to be very specifically formatted.
    #  It must consist of 3 parts: First, the name of the object (I.E the character) being replied to; Secondly, the 'pipe' character ("|"), as a separator; and finally, either "ReplyYes" or "ReplyNo"
    #  Examples: "RandomObject|ReplyNo", "Sign92101|ReplyYes"
     
    for body in cont.sensors['ReadReply'].bodies:
        if body.startswith(own.name): #Thinking forward here; filter by object, assuming the string is formatted correctly 
            if body.partition("|")[2] == "ReplyYes": #Case "Yes"
                own['MsgPtr'] += 2
                ##Again, uncomment and change the "*Lines" and the "DialogueText" as necessary
                #bge.logic.sendMessage("Dialogue", own.name+*Lines[own['MsgPtr']], "DialogueText")
            
            elif body.partition("|")[2] == "ReplyNo":
                own['MsgPtr'] += 1
                ##Again, uncomment and change the "*Lines" and the "DialogueText" as necessary
                #bge.logic.sendMessage("Dialogue", own.name+*Lines[own['MsgPtr']], "DialogueText")
                
            else: #Allow for other cases, even though no such thing is implemented
                raise RuntimeError("Reply must end with either 'ReplyNo' or 'ReplyYes'. No other case is implemented!")
            
            ##Add necessary MsgPtr/other handling code here. For example:
            ##
            ##    if own['msgPtr'] in [2, 8, 9, 13]:
            ##        own['msgPtr'] = 18
            ##    elif own['msgPtr'] in [4, 5, 6, 10, 12]:
            ##        own['msgPtr'] = 14
            ##  Etc.
            
            bge.logic.sendMessage("HideYesNo")


And that’s it! Do note that every Message with the subject “Dialogue” will be interpreted as such (and displayed, if properly formatted). The attached Blendfile has everything set up, and as an extra bonus, also includes a SignController.py script, which you can use to make basic readable signs. :slightly_smiling_face:
DialogueFramework.blend (644.4 KB)