text game

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
turusbango
Prole
Posts: 1
Joined: Wed Nov 29, 2023 3:50 pm

text game

Post by turusbango »

Hello everyone, I'm relatively new to Love2D [I already know the basics of programming logic, understand control structures, functions, etc.]. But what I want to say is that I'm somewhat new to game development. I've learned about the three main functions [load, update, draw]. Does anyone have any tips on how to create a simple text-based adventure game? I was planning to create a demo with 5 scenes [just for testing]. The first scene would be the start screen, the second would display the environment, the third scene would be a choice scene with two options - if you choose option 1, you win; if you choose option 2, you lose. Scene 4 is the ending for option 1, and scene 5 is the ending for scene 2. It's for my sister, and I know I don't necessarily need Love2D to make the game since I could run a game like this through the console, but that would be a bit dull. Does anyone have any tips or even a foundation? I would greatly appreciate it.
Trystan
Prole
Posts: 14
Joined: Fri Nov 24, 2023 9:30 am

Re: text game

Post by Trystan »

Hi, as with all things in programming there are lots of different ways to do this. Below I've put one very quick and easy way to do what you're looking for in love2d.

The gist is that you have a table with all of the text for different scenes in (scenes) and you keep track of the current scene in a variable (currentScene). Every frame the love.draw function will draw to screen the current scene's text.

In love.keypressed (where it's checking for input) you check what scene you're currently in and depending on what key has been pressed advance to the appropriate scene.

I hope this makes sense and is useful. It's almost certainly not the best way to do it but it's hopefully easy enough to follow and adapt for your purposes.

To make it look nicer you cna toy with changing the background colour (love.graphics.setBackgroundColor(red, green, blue)), the text colour (love.graphics.setColor(red, green, blue)) or looking into the different font options.

Code: Select all

-- Load, called at game start
function love.load()
    screenWidth = love.graphics.getWidth() -- get the screen width for use later

    currentScene = 1 -- our current scene
    scenes = {} -- a table that will hold all of the scene text
    -- enter the text for each scene as a string, \n will insert a line break
    scenes[1] = "Do you wan to win?\n\nPress 1 for yes and 2 for no"
    scenes[2] = "Well done, you won, you must be really great at things to have managed to have done so well in this game, what a superstar, I can't say enough good things about you but hopefully that's enough to check the text is wrapping correctly\n\nPress any key to restart"
    scenes[3] = "You lose\n\nPress any key to restart"

end

-- Update, called every frame, we won't actually use this for this game
function love.update(dt)

end

-- We'll use love.keypressed which is called whenever a key is pressed
function love.keypressed(key)
    -- First check what scene we're in. Make sure this is an if .. elseif .. elseif .. chain or we might accidently jump two scenes in one press
    -- scene 1
    if currentScene == 1 then
        if key == "1" then -- if we pressed 1 go to scene 2
            currentScene = 2
        end
        if key == "2" then -- if we pressed 2 go to scene 3
            currentScene = 3
        end
    -- scene 2
    elseif currentScene == 2 then
        currentScene = 1 -- we pressed anything, go to scene 1 (restart)
    -- scene 3
    elseif currentScene == 3 then
        currentScene = 1 -- we pressed anything, go to scene 1 (restart)
    end
end

-- Draw, called every frame after update
function love.draw()
    love.graphics.printf(scenes[currentScene], 5, 5, screenWidth - 10) -- print the current scene text at 5, 5 (wrapping to the screen width)
end
User avatar
dusoft
Party member
Posts: 510
Joined: Fri Nov 08, 2013 12:07 am
Location: Europe usually
Contact:

Re: text game

Post by dusoft »

You can find lot of nice libraries for state / scene management here:
https://github.com/love2d-community/awesome-love2d

Hump gamestate class is easy to work with and works well.
https://hump.readthedocs.io/en/latest/gamestate.html

You can also find other useful helper libraries in the awesome repo such as input handlers etc. You don't have to invent your own (G)UI either, check my https://github.com/nekromoff/layouter or other linked libraries in the repo.
RNavega
Party member
Posts: 251
Joined: Sun Aug 16, 2020 1:28 pm

Re: text game

Post by RNavega »

If I were tasked with creating a text-based game, I would work on it in steps:
1) First get the UI all in place, even if it's a simple font, showing a sample text paragraph ("Lorem ipsum dolor sit amet..."), and make sure that I can break text in lines so I can format paragraphs for things like story, decisions etc.
So that concludes the display of text.

2) Work on the system that reads the player input, like reading a number key for the user to indicate what choice they prefer.
So that concludes the player input.

3) Work on the system that reads a scene file and performs it for the player. From this point on, the engine code would probably stay the same, you would then mostly work on the scene files that define the narrative of your text-based game, including all of the branching (the scenes that fulfill choices like "go left" or "go right" when there's a fork on the road).
Deciding on how you write scenes and what format they're stored in, and how this data is structured internally by the engine, is probably what will take the most time to design.
The most complex of software is still described by a list of instructions or bytecode. Taking inspiration from that, if you use an architecture based on a list of instructions you can definitely describe sophisticated scenes successfully. For instance, among instructions to "show text", "ask question" and such, you would support "jump" instructions to be used when the story needs to branch, so your engine can 'jump' to the (data) point that continues the story based on the decision that the player took, and if a branched route needs to merge back to the main route, then at the end of the branched route you will 'jump' back to the relevant point of the main route.
But to make sure that I can navigate to any point in the story, go back and forth etc to debug and get a feel for the narrative, I would very likely create a simple narrative editor, something like the RPGMaker Event Editor, a tool that lets me do all that and in the end export a "narrative file", an asset that the engine would read and perform.
RNavega
Party member
Posts: 251
Joined: Sun Aug 16, 2020 1:28 pm

Re: text game

Post by RNavega »

I had to try implementing part of what I said, just to get it off my system.
You need a conf.lua file in your directory with this content, to enable the raw console to pop up when you start Löve:

Code: Select all

function love.conf(t)
    t.console = true
    t.window = false
end
Then your main.lua code would be the demo below, which is a simple virtual machine to run the instructions that perform your text adventure:
Image

Code: Select all

-- ==============================================
-- Small text adventure virtual-machine example
--
-- By Rafael Navega (2023)
-- License: Public Domain
-- ==============================================

io.stdout:setvbuf('no')


-- A global table where common information is stored.
local context = {
    speaker = '',
    text = '',
    lastChoice = '',
    narrativeIndex = 0,
}


local jumpComparisons = {
    equals = function(a, b)
        return a == b
    end,

    greater_than = function(a, b)
        return a > b
    end,

    is_empty = function(a)
        return (a and tostring(a) == '' or a == nil)
    end,
    
    is_different = function(a, b)
        if type(b) == 'table' then
            -- Compare 'a' with every ARRAY element of 'b'.
            for index = 1, #b do
                if a == b[index] then
                    return false
                end
            end
            return true
        else
            return a ~= b
        end
    end

    -- (...)
}


local allOperations = {
    SET_SPEAKER = function(parameters)
        context.speaker = (parameters[2] or '')
        return true
    end,

    SET_TEXT = function(parameters)
        context.text = (parameters[2] or '')
        return true
    end,

    SHOW_TEXT = function(parameters)
        if type(context.text) == 'string' then
            if type(context.speaker) == 'string' and #context.speaker > 0 then
                print(context.speaker:upper()..':')
            end
            print(context.text)
        end
        -- Pause until the user hits Enter. Store the answer if this text had a question.
        local answer = io.read()
        if parameters.isQuestion then
            context.lastChoice = (answer:match('%g+') or '')
        end
        print()
        return true
    end,

    JUMP_IF = function(parameters)
        if parameters.comparison then
            local comparisonFunc = jumpComparisons[parameters.comparison]
            if comparisonFunc then
                if comparisonFunc(context.lastChoice, parameters.reference) then
                    -- Advance the narrative (data) index by the steps in the instruction.
                    context.narrativeIndex = context.narrativeIndex + (parameters.steps or 0)
                end
            else
                error('Unknown JUMP_IF comparison: ' .. tostring(parameters.comparison))
            end
        else
            error('No JUMP_IF comparison set.')
        end
        return true
    end,

    END_STORY = function(parameters)
        print('--- THE END ---')
        return false
    end
}


local myNarrativeData = {
    {'SET_SPEAKER', '(???)'},
    {'SET_TEXT', 'Welcome to your biggest nightmare.'},
    {'SHOW_TEXT'},
    {'SET_TEXT', 'Are you sure you want to continue?\n[1] Yes.\n[2] No.'},
    {'SHOW_TEXT', isQuestion = true},
    -- If the lastChoice in the context table is empty or not one of the possible answers,
    -- try asking again to that SHOW_TEXT. If not, continue on.
    -- Note that for negative steps, -1 must be added to account for the JUMP_IF itself.
    {'JUMP_IF', comparison = 'is_different', reference = {'1', '2'}, steps = -2},
    -- If the lastChoice in context is equal to "1" ("Yes"), jump over the next 3 instructions
    -- to avoid the early story finish.
    {'JUMP_IF', comparison = 'equals', reference = '1', steps = 3},
    {'SET_TEXT', 'Hah! Coward!'}, -- 1 step.
    {'SHOW_TEXT'}, -- 2 steps.
    {'END_STORY'}, -- 3 steps.
    -- By this point the user chose "1" ("Yes"), so continue with the story.
    {'SET_TEXT', 'Very well then, welcome. Let me introduce myself.'},
    {'SHOW_TEXT'},
    {'SET_TEXT', 'My name is Jereziah.'},
    {'SHOW_TEXT'},
    {'SET_SPEAKER', 'Jereziah'},
    {'SET_TEXT', 'In this adventure, we will bla bla bla...'},
    {'SHOW_TEXT'},
    -- (...)
    {'END_STORY'},
}


local function runNarrative(narrativeData)
    context.narrativeIndex = 1
    while true do
        local instruction = narrativeData[context.narrativeIndex]
        if instruction then
            local operationName = instruction[1]
            local operation = allOperations[operationName]
            if operation then
                local success = operation(instruction)
                if not success then
                    return
                end
            else
                error(string.format('Unknown operation on index %d: "%s"', index, operationName))
            end
        else
            break
        end
        context.narrativeIndex = context.narrativeIndex + 1
    end
end


runNarrative(myNarrativeData)
Edit: after some more thinking, how I'd improve on this is:
  • Have the possible choices in a question be additional optional parameters to the SHOW_TEXT operation, then it'd format and print the choices automatically, so you wouldn't have to worry about formatting them manually with the SET_TEXT operation.
  • Counting the steps for a JUMP_IF is a bore and a chance for human errors. It's much better to add a new operation type, like {"JUMP_MARKER", name="BeforeQuestion36"}, whose implementation is to record its own data index to the "jumpMarkers" sub-table of the context table. So when you need to jump to a place in the data, instead of an actual number of steps, you'd just go {"JUMP_IF", (...), marker="BeforeQuestion36"}. The JUMP_IF implementation would then set the current index to the index of that named marker, if it exists. The problem is if you want to jump ahead, when future JUMP_MARKERs haven't yet been reached. I guess because of that, you'd need to do a pre-processing of the narrative data list, scanning it for all markers and adding them to the markers table. Only after that would normal execution begin.
  • When writing narrative data by hand, to spare on typing, instead of having the operation names being strings I'd make some variables so I wouldn't have to write them in quotes. That is, instead of {"SET_TEXT", ...}, I'd do:
    local SET_TEXT = "SET_TEXT" and then the tables can be {SET_TEXT, ...} with no need for quotes on the name.
Post Reply

Who is online

Users browsing this forum: No registered users and 52 guests