Adding new features¶
Godot’s architecture is based on object-oriented paradigm, and everything inherits from Object, directly or indirectly.
Since Goost is an engine extension, what applies to Godot development also applies to the process of developing classes in Goost. Refer to Godot’s “Object class” documentation to learn the basics of dealing with Object classes.
The following sections describe the process of writing new classes specifically in Goost. Please refer to Godot’s Engine development section if you are not familiar with engine development in general.
Writing new classes¶
Declaration¶
Components in Goost are comprised of individual classes. For demonstration
purposes, we’ll use FiniteStateMachine
and FiniteStateMachineNode
class
names as examples.
First and foremost, declare new classes in goost.py::classes
, for instance:
classes = {
# ...
"FiniteStateMachine" : "ai",
"FiniteStateMachineNode" : "ai",
# ...
}
The key is the class name, and the value is the component a new class belongs
to. If a new class relates to existing Goost components as seen in
Goost API, you should use the component name as declared in
goost.py::components
list:
components = [
"core/image",
"core/math/geometry",
"scene/physics",
"scene/gui",
"editor",
]
In our case, we don’t have an AI component, so we should add it ourselves:
components = [
# ...
"scene/ai", # AI component which is part of scene.
# ...
]
If one of the classes depend on the other (due to inheritance or composition
design), dependencies must also be declared in goost.py::class_dependencies
:
class_dependencies = {
# ...
"FiniteStateMachine" : "FiniteStateMachineNode",
# ...
}
In our case, FiniteStateMachine
depends on FiniteStateMachineNode
,
because it uses FiniteStateMachineNode
to represent states.
A single class can depend on a set of classes, and Goost will try to resolve all linked dependencies.
Note
Declaring dependencies is required because Goost allows to disable
individual classes via custom.py
file generated with
python goost.py config
command. If any such class is accidentally
disabled, a user must not stumble upon a build or run-time error, so
dependent classes are going to be automatically enabled.
Declaring components and classes like this is mostly needed for documentation generation purposes and customizations, see Configuring the build page for more information.
Implementation¶
Depending on a component, we can choose to implement our classes in core/
,
scene/
, editor/
etc. folders in Goost’s source tree.
In our case, we’ve picked the scene
component as the root component to
implement our first FiniteStateMachine
class in ai
component. Even
though components in Goost don’t always represent structural meaning, we choose
to create a new subfolder under scene/
for the new component:
mkdir -p scene/ai
Create new finite_state_machine.h
header and finite_state_machine.cpp
source files there:
cd scene/ai
touch finite_state_machine.h
touch finite_state_machine.cpp
Note
Make sure that the filenames represent the snake_case
style of original
class names written in PascalCase
. Goost recognizes this pattern and
allows to skip compiling the sources of those classes which got disabled via
custom.py
file.
We’ll skip the actual process of implementing the entire class, but here’s a minimal working implementation that we’ll use for learning purposes:
// finite_state_machine.h
#ifndef GOOST_FINITE_STATE_MACHINE
#define GOOST_FINITE_STATE_MACHINE
#include "scene/main/node.h"
class FiniteStateMachineNode : public Node {
GDCLASS(FiniteStateMachineNode, Node);
protected:
static void _bind_methods();
public:
virtual void _update() {};
virtual void _enter() {};
virtual void _exit() {};
};
class FiniteStateMachine : public Node {
GDCLASS(FiniteStateMachine, Node);
private:
FiniteStateMachineNode *state = nullptr;
protected:
static void _bind_methods();
public:
void set_state(Node *p_state);
Node *get_state() const { return state; }
};
#endif // GOOST_FINITE_STATE_MACHINE
// finite_state_machine.cpp
#include "finite_state_machine.h"
void FiniteStateMachineNode::_bind_methods() {
BIND_VMETHOD(MethodInfo(Variant::NIL, "_update"));
BIND_VMETHOD(MethodInfo(Variant::NIL, "_enter"));
BIND_VMETHOD(MethodInfo(Variant::NIL, "_exit"));
}
void FiniteStateMachine::set_state(Node *p_state) {
ERR_FAIL_NULL_MSG(p_state, "Invalid state.");
auto new_state = Object::cast_to<FiniteStateMachineNode>(p_state);
ERR_FAIL_NULL_MSG(new_state, "The state is not `FiniteStateMachineNode`.");
state = new_state;
}
void FiniteStateMachine::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_state", "state"), &FiniteStateMachine::set_state);
ClassDB::bind_method(D_METHOD("get_state"), &FiniteStateMachine::get_state);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "state"), "set_state", "get_state");
}
Once you copy-paste the implementation, the next step is to tell Goost to
compile those files. Because we’ve introduced a new ai
component, we need to
create a new SCsub
file which is going to collect those sources to compile:
cd scene/ai
touch SCsub
Copy the following contents to SCsub
:
# SCsub
Import("env")
Import("env_goost")
env_goost.add_source_files(env.modules_sources, "*.cpp")
But we’re not done yet. We’ve previously declared ai
as part of scene
component. Usually, you’ll have to look for parent SCsub
and call into our
own SCsub
we’re working on. In this case, lets add the following content
to already existing scene/SCsub
:
if env["goost_ai_enabled"]:
SConscript("ai/SCsub", exports="env_goost")
The goost_ai_enabled
is an construction environment which is automatically
defined in config.py
in Goost. This way, users can skip compiling the
component in the first place if they specify scons goost_ai_enabled=no
via
command-line or via custom.py
file created with python goost.py config
.
After all above steps, you should be able to compile those sources in Goost if
you call scons
command:
cd goost
scons
Next, classes must be registered in ClassDB. Create
register_ai_types.h
and register_ai_types.cpp
where we can register
FiniteStateMachine
and FiniteStateMachineNode
classes respectively:
cd scene/ai
touch register_ai_types.h
touch register_ai_types.cpp
// scene/ai/register_ai_types.h
namespace goost {
void register_ai_types();
void unregister_ai_types();
} // namespace goost
// scene/ai/register_ai_types.cpp
#include "register_ai_types.h"
#include "goost/classes_enabled.gen.h"
namespace goost {
void register_ai_types() {
ClassDB::register_class<FiniteStateMachine>();
ClassDB::register_class<FiniteStateMachineNode>();
}
void unregister_ai_types() {
// Nothing to do yet.
}
} // namespace goost
Goost uses a template specialization technique which allows to register classes
only if they are enabled. If those classes are disabled via custom.py
, then
the implementation of ClassDB::register_class
those will be no-op (as
declared in auto-generated classes_enabled.gen.h
). Unlike components, we
don’t have to use preprocessor defines to conditionally register individual
classes.
Every register_*_types()
callback implementation in Goost requires inclusion
of "goost/classes_enabled.gen.h"
header, where all Goost classes are
included. Due to this, we’ll need to include our FiniteStateMachine
declaration in "goost/goost.h"
as well, which is an umbrella header of all
classes defined in Goost:
// goost.h
// ...
#include "scene/2d/poly_shape_2d.h"
#include "scene/2d/visual_shape_2d.h"
#include "scene/ai/finite_state_machine.h" // FiniteStateMachine
#include "scene/gui/grid_rect.h"
#include "scene/physics/2d/poly_collision_shape_2d.h"
// ...
Just like with parent SCsub
, we also need to call into
register_ai_types()
from within parent scene
component, namely in
register_scene_types()
:
// scene/register_scene_types.cpp
#include "register_scene_types.h"
#include "physics/register_physics_types.h"
#include "ai/register_ai_types.h" // FiniteStateMachine
#include "goost/classes_enabled.gen.h"
namespace goost {
void register_scene_types() {
// ...
#ifdef GOOST_AI_ENABLED
register_ai_types(); // FiniteStateMachine
#endif
}
void unregister_scene_types() {
// ...
#ifdef GOOST_AI_ENABLED
unregister_ai_types(); // FiniteStateMachine
#endif
}
} // namespace goost
Similarly to construction environment variables in SCsub
, Goost
automatically defines a set of preprocessor defines GOOST_*_ENABLED
which
allows us to compile code conditionally.
Once you’ve made the changes above, you should be able to compile Goost again
with scons
command. If everything goes well, you can run the engine with
the following command:
cd goost
python run.py editor
Verify that new classes exists in documentation and are usable in GDScript.
Documentation¶
Once you’ve implemented and built new classes, you can (or rather should) document them. Goost slightly simplifies this process by running the following command:
cd goost
python run.py doc
In our case, you’ll see FiniteStateMachine.xml
and
FiniteStateMachineNode.xml
files generated at doc/
directory. Fill them
out just like other xml
files in the same directory and compile Godot again
with:
scons
If you’d like to see how the built-in documentation looks with new classes from within the Godot’s editor, run:
cd goost
python run.py editor
Note
Unlike in Godot, you don’t have to manually list a new class in
config.py::get_doc_classes()
, because those are collected automatically
in Goost from goost.py::classes
.
Unit tests¶
While implementing new classes, you want to make sure that they work as expected! Running existing unit tests is similar to generating documentation:
cd goost
python run.py tests
Unit tests reside under tests/project
directory. The project
folder is a
master Godot test project which contains goost
directory with tests. Tests
are organized in such a way to closely resemble Goost’s source tree.
Since we’ve introduced ai
component as described in previous sections, we
create res://goost/scene/ai
folder and create
test_finite_state_machine.gd
unit test file there. Goost currently uses
GUT unit testing framework to write and run
tests using GDScript:
extends "res://addons/gut/test.gd"
func test_state():
var fsm = FiniteStateMachine.new()
add_child_autofree(fsm)
var state = FiniteStateMachineNode.new()
fsm.add_child(state)
fsm.state = state
assert_eq(fsm.state, state)
Save the file and run:
cd goost
python run.py tests
To speed up the development, you can run a single unit test file as well:
python run.py tests -t "scene/ai/test_finite_state_machine.gd"
Note
It’s not necessary to start Godot editor to write and run tests. All unit
test files in Goost must be prefixed with test_
to be run from the
command-line interface.
Editor icons¶
The process of adding editor icons for new classes is no different from Godot, please refer to Godot’s Editor icons documentation.
Integrating third-party code¶
If you want to add a feature which relies on external code written by other developers, there are several requirements to resolve and steps to perform:
- The third-party code must be compatible with MIT license.
- Do not use
git
submodules, unless third-party code does not allow to distribute its source code directly, or when it’s more safe to distribute the code via submodules. Whenever possible, always try to bundle the third-party code (the Godot way). - Place third-party code in
goost/thirdparty/
under respective directory. - Compile third-party code from within
goost/thirdparty/SCsub
, compile conditionally if it’s part of existing Goost component. - Make sure to list third-party code in
goost/thirdparty/README.md
. - Update
goost/COPYRIGHT.txt
to comply with third-party license terms. Add new license text if it doesn’t already exist. - Build the engine, run editor and go to
Help
→About Goost
to open Goost “About” dialog. Make sure the third-party component appears at the “Third-party Licenses” tab.