Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit

Homework #7 : Language Hosting

Enabling new genres and achieving flexibility via externalized gameplay logic.

Estimated Time Investment                     Typical Time Allotment                       Team Size

24 hours                                              14 days (check schedule)                            1

Created in 1993 at PUC-Rio in Brazil, Lua has become a language famous for its lightweight and embeddable nature, finding use in embedded systems, video games (Roblox, Garry’s Mod, etc), and even office applications (VLC, Wireshark, etc). While lacking many of the features of peer language Python, Lua uses minimalism to its advantage– maintaining just enough features to remain efficient and enable OOP. Many game engines have been structured with an efficient C++ core that designers access via Lua scripts (Binding of Isaac– left). With the ability to externalize our gameplay logic, new experiences and genres become possible without any changes to the core C++ - powered engine (right)

Pitch

Homework #7 : Language Hosting is a two-week assignment introducing students to the basics of writing a multi-language program. Students will learn to dynamically load scripts at runtime while injecting them with the features and tools necessary to allow for true game customization. In doing so, we will open Pandora’s box and enable our users to fully express their logic in an externalized fashion.

Purpose

The power, efficiency, and interoperability of C++ has made it one of the world’s top languages for four decades, but such features do not come for free. From lengthy compile times and challenging syntax to an advanced memory model and endless series of footguns, game developers have often found themselves investing more time learning the language than on actual content creation. Indeed, game designers, artists, and less technical members of the team may not be able to make any progress at all in C++. With power comes pain.

To address this situation, many game engines will offer a second means of authoring logic– the ability to program in some sort of scripting language– a language that sacrifices efficiency and low-level expressiveness in exchange for a gentler dev experience, faster “compile” times (or no compile times), and convenient standard libraries. The classic “embeddable” / “scripting” language is 1993’s Lua, and it is still in extensive use today.

Thus far our engines have been bound to a single genre– that of a top-down exploration game. By externalizing our gameplay logic and giving game designers the ability to tweak it, our engines will break free of this constraint.

This assignment will--

● Introduce the concept of “host” and “guest” languages, and how we may pass data between them.

● Introduce the venerable Lua scripting language, built specifically for embedding into other software.

● Remove the hard-coded, gameplay-related logic of the top-down game seen in previous weeks.

● Enable a wild degree of gameplay customization not yet seen in any prior assignment.

Tasks

Study the Autograder and Assignment Submission Process

● Autograder General Guide

○ Windows / Visual Studio Guide

○ OSX / XCode Guide

○ Make Guide

Acquire Assignment Dependencies

Do not use external dependencies beyond those from previous assignments and the following (STL is fine)–

New : Lua 5.4.6

● The classic “embeddable language”. Designed for efficiency and minimalism, you need to include the correct header files / link properly with its binaries so that LuaBridge (below) can do its thing.

○ It is possible to forgo LuaBridge when hosting scripts, but it is painful and largely uninformative.

● On windows, you may open a .tar.gz file via 7-zip.

● Lua is distributed in source form. You will need to include the source files in your engine build process (here’s the xcode way), or build the library independently (and then link to it during your engine build).

○ The docs (doc/readme.html) can help a great deal.

● Be careful not to include all lua-related files. Some are not useful for our purposes (lua as a library).

○ After downloading the source, please remove lua.c and luac.c

● On linux, you may link against Lua5.4 by adding -llua5.4 to your make file if you wish.

● WARNING : Only ever #include "lua.hpp" in your c++ codebase. Avoid other lua headers.

○ Such headers are considered “internal” to the lua project, and are not meant for users (us) to mess with or know about or include on their own.

● Having strange compiler issues? Check out the staff’s Lua hints document (WIP)

New : LuaBridge 2.8

● A header-only library that provides convenient data structures and functions for hosting Lua scripts in a C++ environment.

● The lua.hpp header file must always be included before LuaBridge.h. Example (depends on repo).

Lua / LuaBridge Hints Document

Test-Driven Development

Each of the following test suites has a particular focus, and requires the implementation of particular features. You are recommended to clear all test cases in a given suite before proceeding to the next. If you break an older feature or introduce a bug, you may rely on older test suites to warn you of such regressions (You may read about test-driven development, and why it features heavily in industry, here).

TODO : Familiarize Oneself with Basic Lua

● With our engines expanding into Lua hosting, it behooves you to develop a basic feel for how Lua works and what lua scripts look like.

○ Read through some of the “Lua Studies” reading.

○ Consider the Lua Integration guide and watch the lecture recording where we integrate Lua into a C++ Visual Studio project.

TODO : Refactor Engine Codebase

● Much of the gameplay logic and optimization structures from homework #6 will not be useful moving forward. As a result, your codebase will be well-served by a refactoring effort to begin this homework.

a. Tip : How to go about removing “dead code” from out codebase? Make strong use of the C++ compiler to help you.

i. First, make a commit so you may revert your changes if the refactor goes bad.

ii. Second, search through the .h / header files only (ignore the .cpp files for now). Identify variables, data structures, and functions that you suspect may not be relevant anymore. Delete them.

iii. Third, run a build in your IDE. The C++ compiler should now point you to every trace of the now-defunct variables and functions littered throughout your .cpp files. Consider each usage carefully, then delete it.

iv. Fourth, after cleanup, verify that your codebase can now build again. It has lost features from homework #6, but should now be in prime shape to help you begin homework #7.

v. Fifth, enjoy the feeling and confidence of having a cleaner, simpler codebase.

b. Question : How to know what is redundant as we begin homework #7?

i. Region / Spatial / Collision-related data structures and algorithms will not be useful moving forward. In Homework #7, they will be replaced via game-designer-specified Lua code. In Homework #9 they will be replaced with Box2d.

ii. All gameplay and many rendering-related properties will be made redundant as gameplay logic moves into user-customizable scripting logic. Properties worth removing include hp_image, game_over_good_image, game_over_good_audio, game_over_bad_image, game_over_bad_audio, gameplay_audio, player_movement_speed, score_sfx, cam_offset_y, x_scale_actor_flipping_on_movement, and all json-loaded actor-related properties aside from id and name and template.

iii. Some “convenience structures” you may have set up, including a convenience pointer to the “player_actor”, may no longer be useful. Consider removing them.

Test Suite #0 : Component Loading, OnStart, and the Debug Scripting API

This suite contains basic tests exercising your engine’s ability to load Lua scripts (referred to in your engine as components) and provide them debugging-related APIs.

1. We’ll establish Lua as our scripting language to serve our users– game designers / developers.

a. As discussed in the dependencies section above, you will need to both include lua.hpp and link against Lua (or compile the Lua source code) in order to successfully use it.

b. Before loading any scenes, create a new lua_State object using luaL_newstate() and initialize some default libraries via luaL_openlibs()

2. As we move gameplay-related logic into Lua (and out of our c++ core), some properties will vanish–

a. Many of the properties within game.config and rendering.config

i. hp_image, game_over_good_image, game_over_good_audio, game_over_bad_image, game_over_bad_audio, gameplay_audio, player_movement_speed, score_sfx, cam_offset_y, x_scale_actor_flipping_on_movement

b. Most of the actor properties with the exception of–

i. name, template

c. We will also remove the concept of a “player” actor - there may or may not be one in a scene.

i. Maybe a developer will want to make an idle game with your engine? Not all games and genres have an obvious “player” actor, so we shouldn’t enforce having one.

3. Lua scripts may now be placed into resources/component_types/

a. (These scripts will all end with the .lua extension)

b. The scripts are best thought of as “templates” / “blueprints” / “classes” of components.

i. (ie, you will instance / instantiate them and attach these instances to actors)

4. Actors may now be associated with components via the new components object property.

a. (An object of objects. Each component is guaranteed a “key” and a “type”.

b. Example of a .scene file with one actor having two OutputMessage components.

i. “first_component” and “second_component” could be any string key.

ii. (You should think of them as local IDs identifying each component on an actor)

iii. (Q : Why didn’t we just use an array of objects?)

c. OutputMessage type refers to resources/component_types/OutputMessage.lua

d. If a component type is listed in a .scene file but it doesn’t exist, print an error message–

i. error: failed to locate component <component_name>

ii. (then exit immediately with code 0)

5. Study the structure of a valid Lua component type.

a. Abstract example (in file ClassName.lua). Realistic example (in file OutputMessage.lua).

i. Each file will include one table. This table will bear the name of the file.

b. You may “instance” component types like these by creating a new empty Lua table and then establishing inheritance from a base table (the table from the .lua file).

i. Establish a base table for each component type via luaL_dofile().

ii. Create a new empty table to represent the new component instance via luabridge::newTable()

1. Capture the reference in a luabridge::LuaRef variable. Note that these objects have no default constructor, so may not simply create one on its own. Consult lecture / discussion for tips, and be careful not to accidentally instantiate them via a container such as std::vector<luabridge::LuaRef>.

iii. Establish inheritance using the raw lua api – here’s what we do in the staff solution.

6. When an actor is created…

a. Read through its json to find associated components. Add these components to the actor.

i. Tip : Re-read 4.b above.

b. Queue it to have its components’ “OnStart” lifecycle function called at start of next frame.

i. Components may or may not have an OnStart() lifecycle function.

ii. Components should be processed in the alphabetical order of their key.

iii. Note : at game-start, the initial scene will load before the first frame. This means that Actors / components in the initial scene will have their OnStart() called on frame 0 (“the next frame coming”), rather than frame 1.

7. The user’s Lua scripts must be able to call Debug.Log(message)

a. Thanks to LuaBridge, creating new tables and injecting new c++ functions into lua is easy.

b. message is a string that gets printed to std::cout on its own line.

8. The user’s Lua scripts must be able to call Debug.LogError(message)

a. message is a string that gets printed to std::cerr on its own line.

9. If an invalid lua script is instanced, print the following error message and exit immediately (code 0).

a. problem with lua file <lua_filename>

b. Note that <lua_filename> is sans extension (use the path().stem() functions

c. Hint : consider checking the status code returned by luaL_dofile()

10. Lua components may want to access / read their own key from within the script itself.

a. Every lua component will automatically have a self.key variable representing its key.

Test Suite #1 : Component Properties, Overrides, and Inheritance

This suite contains basic tests exercising your engine’s support for overriding the default variable values of Lua components and configuring the order-of-execution of component functions.

Style Suggestions

● Perhaps the most technical, important suite in the entire homework. This will require some thinking.

● What data structures will we need to manage components? Examine the json for some inspiration.

Tasks

1. A component object (the objects within an actor’s components array) may contain other values.

a. (component “1” of type “Transform” seen above has an “x” and “y” property, for instance).

b. After the component is instanced and before any functions are called upon it, inject these “property” values (sometimes called “overrides”) into the component as variables.

c. Example – an actor named “bgm” that has one component (key “1”). The component has type “PlayAudio” (referring to PlayAudio.lua) and two properties that must become variables when said lua script is instanced. It should make sense for a “PlayAudio” component to have properties relating to “clip” (what sound file should I play?) and “loop” (should I play it on loop?).

i. Thus, components are both highly reusable and highly customizable.

2. Within a scene file, we may “override” the values / variables of a component via key-value pairs.

a. Example of a .scene file with two actors with two components

i. (Each OutputMessage component has a different message to output).

ii. (the first OutputMessage component just outputs the default message)

3. Recall how we’ve used actor_templates to configure “default” values of actor properties.

a. Any component on the actor_template will be inherited by the actor as new components.

i. Hint : Use lua table inheritance once more.

ii. Hint : Each actor should have a container to store components (LuaRef*)

1. But which container is best suited?

b. The properties of an inherited component may be overridden on the actor.

i. Q : How to know if a component is a new one or an inherited one from an actor template? Check the component key!

Test Suite #2 : Flexible Referencing

This suite contains basic tests exercising your engine’s support for providing convenient references to lua components (for instance, a reference to the “actor” that owns the component) and actors. These features provide immense convenience to scripters– one of the core purposes of a scripting language in the first place.

Style Suggestions

● What data structures will we need to manage components? Examine the json for some inspiration.

Tasks

1. When a component is instanced, inject a reference to its owning actor via an “actor” variable.

a. self.actor:GetName() should return the owning actor’s name.actor

b. self.actor:GetID() should return the owning actor’s ID.

c. Hint : You may need to inform luabridge of your classes and functions.

i. (you may then inject the actor reference easily)

2. Components may obtain references to other components on the actor.

a. self.actor:GetComponentByKey(key) obtains reference to a component via key.

i. (return nullptr / nil if the key doesn’t exist)

b. self.actor:GetComponent(type_name) obtains reference to component via type.

i. If multiple components exist of a type, return the first (sorted by component key).

ii. (return nullptr / nil if no components of the type exist)

c. self.actor:GetComponents(type_name) obtains reference to all components of type.

i. (return in the form of an indexed table that may be iterated through with ipairs()).

1. Hint: remember that lua tables index starting at 1, not 0

ii. (return an empty table if no components of the desired type exist)

iii. (hint : LuaBridge cannot auto-convert std::vector to a table. Create a table manually.)

3. Sometimes we wish to obtain a reference to other actors in the scene.

a. Actor.Find (name) should return the first actor with the provided name.

i. (return nullptr / nil if no actor with the name exists)

b. Actor.FindAll (name) should return all actors with the provided name.

i. (return in the form of an indexed table that may be iterated through with ipairs()).

ii. (return an empty table if no components of the desired type exist)

c. Hint: add these functions to your Luabridge’s Actor namespace rather than the Actor class (i.e. call beginNamespace(“Actor”))

Test Suite #3 : OnUpdate(), OnLateUpdate(), and Lua Errors

This suite contains basic tests exercising your engine’s support for important, additional Lua lifecycle functions, including OnUpdate() and OnLateUpdate(). These functions allow game designers flexibility on when they want their logic to run and how often (responding to important events in a component’s lifecycle).

Style Suggestions

● Your Actor’s Update() logic will become much more general and abstract with this suite.

Tasks

4. Previously, our engine hard-coded actor “update” logic in C++.

a. Remove this logic. The behavior of actors will now be defined via components alone.

b. If a component contains an OnUpdate(self) function, call it every frame.

c. Make the call after we have finished calling OnStart for every actor and every component.

d. Be careful of calling order. Iterate through actors by ID, then components by key.

5. Sometimes we wish to execute logic after every other component has finished their “update” logic.

a. If a component contains an OnLateUpdate(self) function, call it every frame.

b. Make the call after we have finished calling OnUpdate for every actor and every component.

6. If an error occurs outside of our C++ core, our core must continue to function and a proper error message should be shown to help our game designers know.

a. Within C++, catch exceptions emitted from Lua (OnStart, OnUpdate, OnLateUpdate, etc).

b. Consider using Try-Catch.

c. For each exception, print as shown here (note that “e” is a luabridge::LuaException)

d. Note: when one of our components has an exception, continue executing the functions in the other components

Test Suite #4 : Application Scripting API

This suite contains basic tests exercising your engine’s support for application-related Lua functionality.

Style Suggestions

● The Application.GetFrame() api you’ll create below will serve as an essential tool for many lua scripts. It’s a replacement for the passage of time (an approach necessary to maintain determinism).

Tasks

1. The user’s Lua scripts must be able to call Application.Quit()

a. The application will immediately exit with error code 0

b. (Note, this exiting logic is different than what is required if SDL_Quit event appears in the event queue. In that case, let the current frame finish updating and rendering).

2. The user’s Lua scripts must be able to call Application.Sleep(milliseconds)

a. milliseconds is an integer. The app will sleep for the specified number of milliseconds.

b. Hint : std::this_thread::sleep_for(std::chrono::milliseconds(dur_ms));

i. Defined in header <thread>

3. The user’s Lua scripts must be able to call Application.GetFrame()

a. Return the frame number from Helper::GetFrameNumber()

4. The user’s Lua scripts must be able to call Application.OpenURL(url)

a. url is a string. The app will open the specified web page in the user’s default browser.

b. Hint : use std::system paired with–

i. Windows : start <url> (chosen via _WIN32 preprocessor variable)

ii. OSX : open <url> (chosen via __APPLE__ preprocessor variable)

iii. Linux : xdg-open <url>