Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: My attempt at SFML with Lua scripting  (Read 3960 times)

0 Members and 1 Guest are viewing this topic.

iocpu

  • Newbie
  • *
  • Posts: 10
    • View Profile
My attempt at SFML with Lua scripting
« on: November 24, 2016, 05:00:22 pm »
So just like everyone else, I too have been working on my own 2D game for a few months now. Partially to practice with C++ meta-programming and partially for fun (although SFINAE is lots of fun too). Also, I wanted to create a complete game for once and I think chances are good that it will happen this time! (well, some time)

I'd like to show you how I handled scripting. As the title suggests, the language I picked is Lua... LuaJIT 5.1 to be precise.



For starters, entities. From Lua's point of view, there are only two types of entities: characters and cameras. They share some common behavior (such as movement) but obviously cameras are composed of less components.

While default values for entity attributes are defined in an external JSON file, most of them can be easily overriden by Lua at runtime. Attributes can be accessed and modified with the use of a dot operator, for example:

player.x = 100                -- sets player's X coordinate to 100

local new_y = player.y + 100
player.y = new_y              -- increases player's Y coordinate by 100

print(player.is_moving)       -- prints out whether player is currently moving

There are obviously lots of attributes I can get and set, including entity's collision box offset, current direction or even whether its emitting light.



Methods are used purely for actions an entity can do, such as movement or talking. They can be chained together in order to create a process queue, such that one action will take place after another in a given sequence, for example:

player:move(player.x, player.y + 200)
      :wait(1000)
      :move(player.x + 100, player.y + 200)  -- will first move south, then wait one second, then move east



Each entity has two process queues: one for primary and one for secondary processes. Some behaviors, such as moving or waiting are primary, meaning that if I chain move and wait, wait will happen after move finishes. However, actions such as talking are secondary, so that they can happen simultaneously with primary actions, for example:

player:move(player.x + 150, player.y)
      :talk("hello")
      :move(player.x, player.y)  -- will move east and say text of ID "hello" at the same time, afterwards will move back immediately regardless of whether it's still talking



To make it more complete, entities can also react to certain events that happen to them, such as collision or being triggered by another entity. For example:

player.on_collided = function(entity_id)
    print("Collided with entity of ID " .. id)  -- will print on collision
end

It's also possible to set callbacks for responding to whenever a process starts or ends, for example:

player.on_movement_ended = function(complete)
    if complete then
        player:play_sound("movement_complete.ogg")  -- will play sound if movement process completed successfully
    end
end

This allows for creating complex chains of actions for each entity individually and it's what is needed most of the time. However, the problem arises when creating a cutscene and trying to make one entity do something after another has finished (for example, make entity2 say something after player has moved).

Though this could be achieved with callbacks to on_movement_ended and similar, it would be awfully inconvenient to write all of that boilerplate code every time just to show a simple cutscene. Not to mention that if there was already a callback stored, it would have had to be saved first and then re-set after the cutscene. This is why I created a simple convenience object, simply called Cutscene, for situations just like these. In addition to coordinating actions between different entities, it also disables player's controls, darkens the display and ensures that only one cutscene can be played at any given time.

Cutscene:play(
    { Action.Move, player, player.x, player.y + 300 },
    { Action.Talk, entity2, "hello-player" },
    { Action.Wait, entity2, 1000 },
    function()                     -- I can add arbitrary functions too
        camera.x = player.y
        camera.y = player.y
        camera:record()
    end,
    { Action.Move, camera, entity2.x, entity2.y },
    { Action.Move, entity2, entity2.x - 200, entity2.y }
)





There are also ways to interact with display, audio and interface directly from Lua, for example:

Audio:play_music("theme-music.ogg", true)       -- will play theme music, 'true' will do a fade-in effect
Display.post_processing = PostProcessing.Sepia  -- this is why all my gifs are in sepia :)
Interface:show_message("hello")                 -- will show a pop-up for user to confirm



There are obviously more methods and attributes I can set for devices, though perhaps I will talk about them another time.



At last, there are two more things I wanted to show: event dispatching and save data store.

When writing demo scripts I quickly realized that anything more complex than a very simple game would lead to a lot of coupling and direct dependencies between various parts of the script. That's especially true if action of one entity should cause some other entities to react accordingly. So instead of using global variables and functions all over the codebase, I decided to use an observer pattern by implementing a centralized event dispatcher.

local function callback(what)
    print("Something interesting happened " .. what)
end

Event:subscribe(event.SOMETHING_INTERESTING_HAPPENED, callback)  -- registers the callback

...

-- somewhere else in the code; will notify all subscribers registered for this type of event
local subscribers_called_count = Event:raise(event.SOMETHING_INTERESTING_HAPPENED, "some interesting info")

As far as saving data goes, I wanted to make it very straightforward. There is a globally accessible save data store, which is basically a Lua table that provides a getter and setter for key-value pairs. I can read and write from/to the store as many times as I want but the data is only written to the disk when changes are actually commited (via an explicit method call). So if I want to operate on a value that should be persisent across play sessions, I should read it and write it from/to the save object directly.

-- retrieves progress from save; if nil, provided default value will be returned instead
local story_progress = Save:get(KEY_STORY_PROGRESS, DEFAULT_STORY_PROGRESS)

if story_progress == STORY_PROGRESS_BEGINNING then
    show_some_intro_cutscene()
    Save:put(KEY_STORY_PROGRESS, STORY_PROGRESS_EARLY_GAME)  -- updates progress

    Save:commit()                                            -- writes current contents of save to disk
end

As I was playing around with demo scripts, I realized that most of the times the type of data I was writing to save file were characters' attributes. Moreover, as characters are usually very dynamic and their states change frequently, I needed a way to easily update values associated with entities whenever the data was written to the disk. At first I tried with functions that would serialize each entity and add it to the save file. The problem was, I needed to call these functions every time before calling the commit method. This was not only error-prone but created unnecessary coupling between various parts of the code. Eventually I decided to implement a method that would register an entity and then serialize it and save automatically whenever commit method was called.

Save:register_character(KEY_PLAYER, player)

...

-- somewhere else in the code
Save:commit()  -- player's entity data within the save is automatically updated before changes are written to the disk

Loading a character from save can be accomplished with a single method call too.

Save:read_character(KEY_PLAYER, player)  -- loads player's state from save data



That's all for now. I hope the post wasn't too boring and that perhaps some approaches presented here will be useful to other projects too.

Oh, and one last thing - I didn't do the art. I mean, I wish I could draw like that, but for the time being I'm focused exclusively on the programming side. The sprites and textures come from Lost Garden and OpenGameArt.

Thanks for attention!

felaugmar

  • Guest
Re: My attempt at SFML with Lua scripting
« Reply #1 on: November 24, 2016, 07:35:30 pm »
That's all for now. I hope the post wasn't too boring and that perhaps some approaches presented here will be useful to other projects too.
The post wasn't boring, it was really good to read it actually ;D

Well, I liked your post, really, it was very interesting ;)
Please, keep posting about it  ;D

FRex

  • Hero Member
  • *****
  • Posts: 1845
  • Back to C++ gamedev with SFML in May 2023
    • View Profile
    • Email
Re: My attempt at SFML with Lua scripting
« Reply #2 on: November 24, 2016, 08:36:36 pm »
I don't have time to read it all right now so sorry if you answered these already in the text.
I'm a big fan of Lua, especially 5.1 due to it's compatibility with JIT and how well established it is and I dislike the 5.3 change to numbers and 5.2 is not really a big deal to me.
I'm even writing a paper on its design, implementation, bytecode, VM, goals, philosophy, history, etc. ;)
Did you use any binding or helper or just Lua C API itself?
Are you using any FFI features or just using LuaJIT like you would PUC's Lua?
Back to C++ gamedev with SFML in May 2023

 

anything