Jump to content

Branching dialogue


Angelwolf
 Share

Recommended Posts

Hi all,

I'm looking to implement dialogue options into my current game, but I'm wondering what the best way to do this would be?

I'm not great with Lua as some of you older posters may remember, however I often manage to work my way though things using methods that make sense to me but are not necessarily viable or efficient.

So far, I have been using a system with 'flags' whereby depending on the answer the player gives (in the way of pressing F1, F2, F3 etc), a flag is set as true or false which then branches the conversation depending on the answers given. Presently, however, I have hit a snag where my code does not allow me to branch past the second question I need to re-use keys in order to progress the discussion.

For example:

Question 1: How are you feeling today?
(answer) F1: I'm great!
(answer) F2: I'm okay
(answer) F3: I feel sad

Supposing the player presses F3...

Question 2: Why do you feel sad?
(answer) F1: The weather is bad
(answer) F2: My cat ran away
(answer) F3: I don't like my salary

The problem here is that because the player pressed F3 in question 1, the next question given is question 2 (which is correct) but it immediately answers as F3 again.

Can anyone shed some advice on this?

Link to comment
Share on other sites

3 hours ago, Thirsty Panther said:

Hard to tell without seeing your code but I'm guessing you are not resetting your variable for the key press for the first question.

ie Question one: Answer = "F3"

Question two: Answer = still equals "F3" from question one.

Hope this helps.

Good to have you back.

This is exactly right, and what I'm having trouble working out how to fix (because suppose I change the response key to F10, the issue goes away because the initial requirements are still met, but with a different 'fire' button). I'll see if I can get my code online very soon so you can at least see what I mean and also marvel at its lack of elegance.

Thanks for the welcoming reply; It's been a busy year or two. Now married with a beautiful daughter! 

Link to comment
Share on other sites

Script.enabled=true--bool "Enabled"
Script.npcname=NPC--string "NPC Name"
Script.hudFont = nil -- REMOVE IF BROKEN
Script.indialogue=false--bool
Script.rude=false--bool
Script.askname=false--bool
Script.asklocation=false--bool
Script.introdone=false--bool

Script.intro = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - Intro.tex") --texture for the dialogue box (initial intro)
Script.introdonerude = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - introrude.tex") --texture for the dialogue box (if they player was rude, show this intro next time they interact)
Script.introdone = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - introdone.tex") --texture for the dialogue box (intro once the player has already spoken to the NPC once before)
Script.askedname = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - name.tex") --texture for the dialogue box (NPC tells player their name)
Script.askedlocation = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - location.tex") --texture for the dialogue box (NPC tells player where they are)

diagboxcolor = Vec4(1,1,1,0.8)
diagboxLength = context:GetWidth() --set dialogue box length to the same length as the game window
diagboxHeight = context:GetHeight()/3 --set dialogue box height to one third of the height of the game window
xback = 0 --set the back (left side) (x) of the dialoge box to the left of the game window
yback = context:GetHeight() -diagboxHeight --set the back (bottom) (y) of the dialogue box to the bottom of the game window


function Script:Start()
self.hudFont = Font:Load("Fonts/hudfont.ttf",12) --REMOVE IF BROKEN, IT IS FOR THE HUD
self.introdone = false
self.rude = false
self.askname = false
self.asklocation = false
end

function Script:Use()
	if self.indialogue == false
		then self.indialogue = true
			else
		if self.indialogue == true
			then self.indialogue = false
		end
	end
--self:Toggle()
end

function Script:Toggle()--in

end

function Script:PostRender(context)
	context:SetBlendMode(Blend.Alpha)


	-- THE BELOW IS FOR THE INITIAL INTRODUCTION DIALOGUE
	if self.indialogue == true and self.introdone == false and self.rude == false and self.askname == false and self.asklocation == false then
		context:SetColor(diagboxcolor) --colour parameters for hudbox, and for alpha
		context:DrawImage(self.intro,xback,yback,diagboxLength,diagboxHeight) --draw the dialogue box in the screen (within the Script:PostRender(context) function					
		playerCanMove = false
			if self.indialogue == true and self.introdone == false and self.rude == false and self.askname == false and self.asklocation == false and window:KeyDown(Key.F1) then
				self.indialogue = true
				self.introdone = true
				self.rude = false
				self.askname = true
				self.asklocation = false
				else 
					if self.indialogue == true and self.introdone == false and self.rude == false and self.askname == false and self.asklocation == false and window:KeyDown(Key.F2) then
						self.indialogue = true
						self.introdone = true
						self.rude = false
						self.askname = false
						self.asklocation = true
						else
							if self.indialogue == true and self.introdone == false and self.rude == false and self.askname == false and self.asklocation == false and window:KeyDown(Key.F3) then
							self.indialogue = false
							self.introdone = true
							self.rude = true
							self.askname = false
							self.asklocation = false
							end
					end
			end
	else if	-- THE BELOW IS FOR IF THE PLAYER PRESSES F3 (rude) IN THE INITIAL INTRODUCTION DIALOGUE
		self.indialogue == true and self.introdone == true and self.rude == true and self.askname == false and self.asklocation == false then
		context:SetColor(diagboxcolor) --colour parameters for hudbox, and for alpha
		context:DrawImage(self.introdonerude,xback,yback,diagboxLength,diagboxHeight) --draw the dialogue box in the screen (within the Script:PostRender(context) function					
		playerCanMove = false
			if self.indialogue == true and self.introdone == true and self.rude == true and self.askname == false and self.asklocation == false and window:KeyDown(Key.F1) then
				self.indialogue = true
				self.introdone = true
				self.rude = false
				self.askname = true
				self.asklocation = false
				else 
					if self.indialogue == true and self.introdone == true and self.rude == true and self.askname == false and self.asklocation == false and window:KeyDown(Key.F2) then
						self.indialogue = true
						self.introdone = true
						self.rude = false
						self.askname = false
						self.asklocation = true
						else
							if self.indialogue == true and self.introdone == true and self.rude == true and self.askname == false and self.asklocation == false and window:KeyDown(Key.F3) then
							self.indialogue = false
							self.introdone = true
							self.rude = true
							self.askname = false
							self.asklocation = false
							end
					end
			end
	else if	--THE BELOW IS FOR IF THE PLAYER PRESSES F1 (name) IN THE INITIAL INTRODUCTION DIALOGUE
	    self.indialogue == true and self.introdone == true and self.rude == false and self.askname == true and self.asklocation == false then
		context:SetColor(diagboxcolor) --colour parameters for hudbox, and for alpha
		context:DrawImage(self.askedname,xback,yback,diagboxLength,diagboxHeight) --draw the dialogue box in the screen (within the Script:PostRender(context) function					
		playerCanMove = false
			if self.indialogue == true and self.introdone == true and self.rude == false and self.askname == true and self.asklocation == false and window:KeyDown(Key.F1) then
				self.indialogue = true
				self.introdone = true
				self.rude = false
				self.askname = false
				self.asklocation = true
				else 
					if self.indialogue == true and self.introdone == true and self.rude == false and self.askname == true and self.asklocation == false and window:KeyDown(Key.F2) then
						self.indialogue = false
						self.introdone = true
						self.rude = false
						self.askname = false
						self.asklocation = false
						else
					end
			end
	end		
	end 
	end --end of else ifs

	if self.indialogue == false then
		playerCanMove = true
	end

end

Here's my trainwreck of code. Hope it helps to understand what I'm going for.

I'm sure there must be a better and more simple way to do this, and I'm willing to re-write this entire thing if need be.

Link to comment
Share on other sites

Just off the top of my head, having anything bug draw logic in PostRender() would be not ideal. Have your logic in UpdateWorld() and use PostRender() to just draw 2d images or text of the results. This means don't have window:KeyDown() in PostRender().

 

The second thing is using window:KeyDown(). The code is fairly messy to read but remember that KeyDown() will register true each frame if it's held down and it's hard for a person to press a key and have KeyDown() see it for only 1 frame. Try using KeyHit() instead maybe because that only registers the key press for 1 frame no matter if they hold it down. So that might help solve your issue of the question getting answered the same when they press a key.

 

As far as structure goes, this is clearly, I think you know this, a mess of code which isn't very flexible and hard to debug. There is so much code duplication as well which will lead to hard to find bugs as soon as you start changing anything.

 

You should probably go back to the drawing board with how to approach the requirements. Anytime you find yourself using just a **** load of boolean flags it's a good indication there is a better way. A bunch of boolean flags generally means you're trying to control state. So something to possibly look into is a state machine.

 

The practical idea of state machines is that it allows you to change states and each state can be nicely isolated. In the case of a Lua library I made for LE this means each state has functions. Enter/Exit/Update(). When you change from 1 state to another the state you were in calls the Exit() function you defined for that state, then calls the Enter() function for the new state you changed to, then continuly calls Update() for the new state until you change state in which the same thing happens. Exit the old state, enter the new state, then Update() on the new state over and over again where you are looking for the user input or something to change to another state. To look into this go to the Workshop from the LE editor and search on state machine and you'll see my FSM. Download it. In the description I have links to videos explaining how it works.

 

Something else to look at is the structure of your data. In this case it seems like you have questions that can lead to other questions and each question can have 3 answers. So think about how you could structure your data for that. This would be a table that holds question and answers and nested tables under as well to any level you want.

  • Like 2
Link to comment
Share on other sites

6 hours ago, Rick said:

Very useful stuff I don't want to quote in a massive post

Thanks Rick. I did see your LUA library (I understand this is a paid item - no problem). If I were to utilise this, would you be happy to walk me through any troubles I have if I cannot work it out after watching the videos?

I think having isolated states would be hugely beneficial and would be the perfect solution for my problem.

Something I need to consider is that my game will have a heavy emphasis over dialogue and choices. Will your library be suitable for dialogue on a large scale?

Presently I'm using images for dialogue and choices because I find it easier to work with. I could move to text (which might prove better) but I wasn't sure about word wrapping and adjustment for if the player runs at different resolutions. If you would be happy to include a specific dialogue module to your library, or even make one for me personally, I wouldn't object to pay you (or someone) for such a commission, depending on a quote.

Link to comment
Share on other sites

My FSM is free. I would be willing to help yes.

 

You for sure want to go with text vs images but yes that does mean you'll want some kind of generic code (library) to deal with all the issues that come with that (resolutions, line breaks etc). This can get sticky because it's all about font size and in LE you have to load fonts to get different sizes which is a pain. I have a UI library (it's sort of complicated now) where I have a label control where you size the rectangle where the text should fit and on load of the game it loops through a bunch of font sizes and loads them and tries to figure out what size fits best inside the rectangle and the rectangle itself is resolution independent (the size of it is relative to the screen resolution so it always looks the same on any resolution). You'll have the same issue with your images though because the size of them aren't resolution independent and you've had to account for that if you want that way too.

 

I can release my UI library. It's pretty powerful as it handles all this stuff for you but that comes at a cost of complexity and I don't have any tutorials on it. I have some time tomorrow so I'll upload it then and make a youtube video on it.

  • Like 2
Link to comment
Share on other sites

Thank you for your assistance with this. Our baby daughter had a good sleep which gave me a good night of sleep for a change. I've gone back to the drawing board with this and it appears to work, and is much easier to read. I'm sure there is still a better way, but this works - so thank you for your help. Any comments are very welcomed.

 

import "Scripts/Functions/ReleaseTableObjects.lua"

Script.enabled=true--bool "Enabled"
Script.npcname=NPC--string "NPC Name"
Script.indialogue=false--bool

Script.intro = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - Intro.tex") --texture for the dialogue box (initial intro)
Script.introdonerude = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - introrude.tex") --texture for the dialogue box (if they player was rude, show this intro next time they interact)
Script.introdone = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - introdone.tex") --texture for the dialogue box (intro once the player has already spoken to the NPC once before)
Script.askedname = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - name.tex") --texture for the dialogue box (NPC tells player their name)
Script.askedlocation = Texture:Load("Dialogue/Downtown/Paradise Apartments/paradise apartments security guard - location.tex") --texture for the dialogue box (NPC tells player where they are)
Script.dialogue = Texture:Load("Dialogue/Downtown/Paradise Apartments/dialoguetest.tex")

diagboxcolor = Vec4(1,1,1,0.8) --colour parameters for hudbox, and for alpha
diagboxLength = context:GetWidth() --set dialogue box length to the same length as the game window
diagboxHeight = context:GetHeight()/3 --set dialogue box height to one third of the height of the game window
xback = 0 --set the back (left side) (x) of the dialoge box to the left of the game window
yback = context:GetHeight() -diagboxHeight --set the back (bottom) (y) of the dialogue box to the bottom of the game window


function Script:Start()
	self.dialogue = self.intro --Set this as the default dialogue text
end

function Script:Use()
	if self.indialogue == false then --Check to see if the player is available for dialogue
		self.indialogue = true --Register that the player is in a dialogue
		playerCanMove = false --Stop player form moving when they're in a dialogue
	end
end

function Script:UpdateWorld()--in
	if self.indialogue == true and self.dialogue == self.intro then --Give player the initial introduction
		if window:KeyHit(Key.F1) then --Player asked name
			self.dialogue = self.askedname --Change graphic to askedname
			else if window:KeyHit(Key.F2) then --Player asked location
				self.dialogue = self.askedlocation --change graphic to askedlocation
				else if window:KeyHit(Key.F3) then --Player told NPC to buzz off
					self.dialogue = self.introdonerude --Change graphic to a rude introduction next time (player and NPC have met before)
					self.indialogue = false --Remove the player form dialogue
				end
			end
		end
	end

	if self.indialogue == true and self.dialogue == self.askedname then --NPC answered with their name
		if window:KeyHit(Key.F1) then --Player asked location
			self.dialogue = self.askedlocation --Change graphic to askedlocation
			else if window:KeyHit(Key.F2) then --Player said goodbye
				self.dialogue = self.introdone --Change graphic to a nice introduction next time (player and NPC have met once before)
				self.indialogue = false --Remove the player from dialogue
			end
		end
	end

	if self.indialogue == true and self.dialogue == self.askedlocation then --NPC answered with location
		if window:KeyHit(Key.F1) then --Player asked name
			self.dialogue = self.askedname --Change graphic to askedname
			else if window:KeyHit(Key.F2) then --Player said goodbye
				self.dialogue = self.introdone --Change graphic to a nice introduction next time (player and NPC have met once before)
				self.indialogue = false --Remove the player from dialogue
			end
		end
	end

	if self.indialogue == true and self.dialogue == self.introdone then --Player and NPC have met before
		if window:KeyHit(Key.F1) then --Player asked NPC to remind them of their name
			self.dialogue = self.askedname --Change graphic to askedname
			else if window:KeyHit(Key.F2) then --Player asked NPC to remind them of their location
				self.dialogue = self.askedlocation --Change graphic to askedlocation
				else if window:KeyHit(Key.F3) then --Player said goodbye
					self.indialogue = false --Remove the player from dialogue
				end
			end
		end
	end

	if self.indialogue == true and self.dialogue == self.introdonerude then --Player and NPC have met before and player told NPC to buzz off
		if window:KeyHit(Key.F1) then --Player apologised and asked for name
			self.dialogue = self.askedname --Change graphic to askedname
			else if window:KeyHit(Key.F2) then --Player apologised and asked for location
				self.dialogue = self.askedlocation --Change graphic to askedlocation
				else if window:KeyHit(Key.F3) then --Player told NPC to go away
					self.indialogue = false --Remove the player from dialogue
					self.dialogue = self.introdonerude --Remember that Player has been rude to NPC
				end
			end
		end
	end

	if self.indialogue == false then
		playerCanMove = true
	end

end

function Script:PostRender(context)
	context:SetBlendMode(Blend.Alpha)
	if self.indialogue then
		context:SetColor(diagboxcolor) 
		context:DrawImage(self.dialogue,xback,yback,diagboxLength,diagboxHeight) --draw the dialogue box in the screen (within the Script:PostRender(context) function					
	end
end

function Script:Disable()--in
	self.enabled=false
end

function Script:Enable()--in
	self.enabled=true
end

function Script:UpdatePhysics()
	
end

function Script:Release()
	ReleaseTableObjects(self.sound)
	if self.loopsource then
		self.loopsource:Release()
		self.loopsource=nil
	end
	self.sound=nil
end

 

Link to comment
Share on other sites

Load your dialogs from lua files(a table) and have them rendered.You need something generic for all kinds of dialogs and depths.

 

We have something like this in the forth project using leadwerks gui.

The choices table tells the next sub dialogs, and each dialog line has a function handler that gets called when option was selected.

 

TerminalDialog = {
    ["11"] = { speaker = "Monolith", text = "Terminal active. Awaiting commands: ", navButtonType = "End Dialog", choices = {"Print command", "Print command input", "Select operation", "Input command data", "Clear command", "Execute command"}, func = UpdateTerminalDialogData},
        ["1111"] = { speaker = "Monolith", text = "Monolith command:\n\n N " .. mNorthInput .. "\n S " .. mSouthInput .. "\n E " .. mEastInput .. "\n W " .. mWestInput .. "\n U " .. mUpInput .. "\n D " .. mDownInput, navButtonType = "End Dialog", choices = {}, func = nil},
        ["2111"] = { speaker = "Monolith", text = "Current databanks content:\n\n N " .. northInput .. "\n S " .. southInput .. "\n E " .. eastInput .. "\n W " .. westInput .. "\n U " .. upInput .. "\n D " .. downInput, navButtonType = "End Dialog", choices = {}, func = nil},
        ["3111"] = { speaker = "Monolith", text = "Pick type of operation performed when pushing data:", navButtonType = "End Dialog", choices = {"Concatenation", "Addition", "Substraction", "Multiplication"}, func = nil},
            ["123111"] = { speaker = "Monolith", text = "Concatenation active", navButtonType = "End Dialog", choices = {}, func = function() selectedOperation = 0 end},
            ["223111"] = { speaker = "Monolith", text = "Addition active", navButtonType = "End Dialog", choices = {}, func = function() selectedOperation = 1 end},
            ["323111"] = { speaker = "Monolith", text = "Substraction active", navButtonType = "End Dialog", choices = {}, func = function() selectedOperation = 2 end},
            ["423111"] = { speaker = "Monolith", text = "Multiplication active", navButtonType = "End Dialog", choices = {}, func = function() selectedOperation = 3 end},
        ["4111"] = { speaker = "Monolith", text = "All data pushed", navButtonType = "End Dialog", choices = {}, func = PushTerminalData},
        ["5111"] = { speaker = "Monolith", text = "Databanks cleared", navButtonType = "End Dialog", choices = {}, func = ClearTerminalDatabanks},
        ["6111"] = { speaker = "Monolith", text = "Execute command", navButtonType = "End Dialog", choices = {}, func = ExecuteCommand}
}

 

Its 2 lua classes Dialog and DialogManager.I can send you the files if you want to have a look.

Just trying to give you some ideas.

  • Like 1

I made this with Leadwerks/UAK:

Structura Stacky Desktop Edition

Website:

Binary Station

Link to comment
Share on other sites

20 hours ago, aiaf said:

 


TerminalDialog = {
    ["11"] = { speaker = "Monolith", text = "Terminal active. Awaiting commands: ", navButtonType = "End Dialog", choices = {"Print command", "Print command input", "Select operation", "Input command data", "Clear command", "Execute command"}, func = UpdateTerminalDialogData},
        ["1111"] = { speaker = "Monolith", text = "Monolith command:\n\n N " .. mNorthInput .. "\n S " .. mSouthInput .. "\n E " .. mEastInput .. "\n W " .. mWestInput .. "\n U " .. mUpInput .. "\n D " .. mDownInput, navButtonType = "End Dialog", choices = {}, func = nil},
        ["2111"] = { speaker = "Monolith", text = "Current databanks content:\n\n N " .. northInput .. "\n S " .. southInput .. "\n E " .. eastInput .. "\n W " .. westInput .. "\n U " .. upInput .. "\n D " .. downInput, navButtonType = "End Dialog", choices = {}, func = nil},
        ["3111"] = { speaker = "Monolith", text = "Pick type of operation performed when pushing data:", navButtonType = "End Dialog", choices = {"Concatenation", "Addition", "Substraction", "Multiplication"}, func = nil},
            ["123111"] = { speaker = "Monolith", text = "Concatenation active", navButtonType = "End Dialog", choices = {}, func = function() selectedOperation = 0 end},
            ["223111"] = { speaker = "Monolith", text = "Addition active", navButtonType = "End Dialog", choices = {}, func = function() selectedOperation = 1 end},
            ["323111"] = { speaker = "Monolith", text = "Substraction active", navButtonType = "End Dialog", choices = {}, func = function() selectedOperation = 2 end},
            ["423111"] = { speaker = "Monolith", text = "Multiplication active", navButtonType = "End Dialog", choices = {}, func = function() selectedOperation = 3 end},
        ["4111"] = { speaker = "Monolith", text = "All data pushed", navButtonType = "End Dialog", choices = {}, func = PushTerminalData},
        ["5111"] = { speaker = "Monolith", text = "Databanks cleared", navButtonType = "End Dialog", choices = {}, func = ClearTerminalDatabanks},
        ["6111"] = { speaker = "Monolith", text = "Execute command", navButtonType = "End Dialog", choices = {}, func = ExecuteCommand}
}

 

This looks quite interesting, thank you. How, exactly, does it work?

 

Link to comment
Share on other sites

2111 means: option index 2 depth 1 parent 11

323111 means: option index 3 depth 2 parent 3111

This way the key is unique and i have a variable depth so i can access things in the above table.

 

At begin we are at ["11"] i just draw the corresponding choices table:

"Print command", "Print command input", "Select operation", "Input command data", "Clear command", "Execute command"

Player click Select operation (that is index "3111"), draw the choices if they exits and execute the corresponding func.

dialogManager.goto("3111")

Its generic, works for any depth, single inconvenient is the key can get pretty long for higher depth.

I could hide that from the user, by using a file with tab as depth specifier that after it generates the file as above.

 

Ill send you the code when i come back to my computer.

 

 

I made this with Leadwerks/UAK:

Structura Stacky Desktop Edition

Website:

Binary Station

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...