About this blog
Personal Game Engine Development
Entries in this blog
This is the second posting that deals with interfacing C++ with a Python scripting layer. Part 1 can be found here.
The focus in this post relates to the design I chose to follow for wrapping Python callback functions in C++. The design objective here is to ensure that a callback function can be quickly defined in python, while also not requiring any recompilation of the c++ UI library. This last feature is important, because in theory the user interface should be configurable and extendable by advanced end users of the game engine. A lot of my inspiration of this train of thought can be attributed to World of Warcraft's extremely customizable user interface.
To setup this example, consider the following xml file that defines a simple GUI, with a window containing a button. The button defines a callback to be executed when the button is pressed, that links to a function defined in Python.
When constructing this class, a generic interface must be defined that are components of GUI elements. In my codebase, currently all Python standalone functions like this inherit from this callback process.
PythonCallback(const std::string& module, const std::string& callback);
Notice _module and _callback are PyObjects. To rehash from my earlier entry, I personally prefer to forward declare this PyObjects rather than use a #include to ensure that python.h is not included unless it is absolutely necessary (e.g., in the cpp implementation file!).
// forward declare PyObject
// as suggested on the python mailing list
typedef _object PyObject;
Much of the Python C API setup is done in the constructor of this abstract class. The guts of this is largely derived from the Python C API documentation, particularly in the following tutorial:
The primary function here is to create the _callback and _module instances and maintain proper memory management of our PyObject references.
PythonCallback::PythonCallback(const std::string &module, const std::string &callback)
pyName = PyString_FromString(module.c_str());
_module = PyImport_Import(pyName);
_callback = PyObject_GetAttrString(_module, callback.c_str());
/* _callback is a new reference */
if (_callback && PyCallable_Check(_callback))
Now with the interface defined, we can define Functor's (http://www.parashift....html#faq-33.15) that wrap the more complicated Python C API code. The following is the class declaration of an example PythonCallbackVoid class.
// wraps Python callbacks that require no arguments and return an int
class PythonCallbackVoid : public PythonCallback
PythonCallbackVoid(const std::string& module, const std::string& callback);
virtual int operator()();
Since the PythonCallback base class setups the _callback PyObject, all that is required of the child class is to call the _callback function.
int PythonCallbackVoid::operator ()()
// throw or assert
PyObject* pyValue; // PyObject that contains the return value.
// call into the callback function
pyValue = PyObject_CallObject(_callback, 0);
long retVal = 0;
retVal = PyInt_AsLong(pyValue);
Pulling it all together:
I'll wrap up this entry with an example to summarize these classes. First, I'll assume that there is a Python module defined in the following directory structure
/bin (working directory)
__init__.py (initialize the ui module)
If the use of __init__.py seems foreign to you, be sure to check out Python's module documentation: http://docs.python.o...al/modules.html
(This is one of the strengths of Python; their online documentation is outstanding). In a nutshell, the __init__.py files are required to inform Python that the directory contains packages.
Inside uitest.py, a simple function called test:
print 'ui test successful'
Now in C++, we can call this function using the new PythonCallbackVoid functor fairly easily:
// Note: Py_Initialized() must be called directly prior to this block of code
// (see PythonManager, documented in Part 1)
PythonCallbackVoid callback("ui.uitest", "test");
int retVal = callback(); // returns 42
Next entry, I'll start discussing Boost.Python, for wrapping C++ classes for use in Python scripts. Thanks for reading.
I've recently decided it was time to gain a bit more flexibility in my c++ codebase by adding support for a scripting interface. My initial use case was to enable my homegrown gui system (in c++) to offload callbacks and general customization to a scripting layer, rather than having to hard code a user interface in C++. I chose Python as my scripting language, due to my years of experience with this language.
With the GUI/addon system use case in mind, I need my C++ code to call into a Python script/module and I also need my Python scripts to interact with C++ objects. Here's a simple example that I've got in mind for this process:
# import the pyd that interfaces python with my c++ library
from az_gui import *
def onCancelButton(self, event):
Event handler for cancel button.
window = self.getWindow()
I've decided to split this topic into several postings. This first post covers the ground work necessary for calling into Python scripts. The second posting will review a PythonCallback wrapper class that is designed to encapsulate the Python C API code for calling function callbacks in python modules. After that I'll have a post or two on my Boost.Python experiences that expose my C++ libraries to Python (e.g., allowing for Python code to call the C++ class, Window as shown in the example above).
C++ calls into Python scripts:
Went with the Python C API for communication in this direction, since it was a manageable process. I'm resistant to adding 3rdParty dependencies to my project, so this seemed like the best I could do. On a side note, my original goal was to write my own Python C API wrappers to also exposing my c++ classes to Python scripts, but that panned out to be an enormous undertaking that I'm not ready to tackle yet. (this discussion is saved for another post).
There were a few catches along the way that made the implementation less straightfoward. For starters, a standard install of Python does not have a debug library. As such, some macro magic needs to be performed to include Python.h header file.
// Since Python may define some pre-processor definitions which affect
// the standard headers on some systems, you must include Python.h before
// any standard headers are included.
It is my preference to keep this Python.h ugliness inside the cpp files, so my next trick involved forward declaring PyObject:
// forward declare PyObject
// as suggested on the python mailing list
typedef _object PyObject;
Pulling it all together:
To manage the Python C API, I created a PythonManager class to encapsulate the Python interpreter session used within my codebase.
// add directories to the python interpreter's sys.path
// to allow for Python scripts to locate script directories.
bool addSysPath(const std::string& relativePath);
bool PythonManager::addSysPath(const std::string& relativePath)
ss ss ss ss ss ss ss ss ss ss
int retval = PyRun_SimpleString(ss.str().c_str());
if (retval != 0)
return retval == 0;
Finally, this PythonManager is called from outside my "main loop":
PythonManager* python = new PythonManager();
Check back for follow on summaries on the remaining implementation details (PythonCallback wrapper class, and Boost Python).
Since I'm the sole programmer on my own personal game engine, I didn't feel much pressure in using configuration management software. I had at one point been reliant on regular backups by simply zipping the entire solution directory and timestamping the zip file. I keep a changelog that summarizes the changes made to the code base in order to hint at the state of the code snapshot in case I ever needed to roll back to that version. I've was never thrilled with solution; 95% (buttpull number) of each zip contained code that had not changed, rolling back could result in lots of snooping around to find the correct version, the backup process was time consuming, no diff mechanism to verify that I actually wanted to keep all changes made for this version snapshot.
A while back, I found a good opportunity to switch over to subversion for all of my revision needs. I had setup a svn repository on a remote ubuntu server in the past, but I hardly keep that old clunker running so it was a pain to power up a machine each time I wanted to check in some code. My newer approach was to create a repository on my development laptop and check in code locally. The plan is regularly backup this repository onto another machine for redundancy purposes. Since the repository is stored on my local development machine, I know it will always be online for code checkins. If the machine happens to be powered down, I certainly won't be coding.
To setup this standalone svn repository, I used Tortoise SVN (http://tortoisesvn.tigris.org/). This was the only download needed. After installing and rebooting, I created my repository by creating a folder named "svn_repository", right clicking on that new folder, and selecting TortoiseSVN->"Create Repository Here".
The next step required is setup the structure of the svn repository. I prefer to use the trunk, braches, tags structure for my repository. To do this, I created a temporary directory (C:\svn_temp) and placed three empty subfolders here, svn_temp\trunk, svn_temp\branches, and svn_temp\tags. Right click on svn_temp and select "Tortoise SVN -> Import ...". For the comment, I just said something along the lines of "Repository folder structure". The URL of the repository in my case was "file:///c:/projects/svn_repository", which was the top directory for my repository.
The final step involves the importing of my current code base. Since the trunk is the active (and sometimes unstable) HEAD of the repository, I wanted to import my first batch of code into the trunk. I didn't see this code base as being at any major version, so that is why I chose a checkin into the trunk rather than the tags directory. First I created a directory called svn_workingcopies. I right clicked on this new folder and selected Checkout. For the repository URL, I specified ""file:///c:/projects/svn_repository/trunk", since I was planning on working on the trunk. The operation should complete successfully, and there won't be any new contents added to this svn_workingcopies directory since the trunk is currently empty. I copied and pasted my entire code base from the old location (that was archived with zip files), to the new svn_workingcopies directory. To add this codebase to svn, I right clicked on the top level directory of my newly copied project ("c:\projects\svn_workingcopies\azrial") and selected "Tortoise SVN -> Add". From here I weeded out (unchecked) any file that was generated (game log files, Debug directory contents, Release Directory contents, etc). I didn't want stuff in the repository that would change each time I ran my project. Now that everything is added, it is ready to be committed to the repository. This was done by right clicking on the project and selecting "SVN Commit". After double checking that I was checking in only the essential files, I hit ok and was left with a full code base in the trunk directory of my repository.
With all of this in place, I will be creating tags at major code milestones, creating branches when I plan to do some experimentation on the codebase, and performing diffs on files before doing commits to verify that no "test code" sneaks into the repository.
By request, I'm attaching some sample source code that demonstrates the topics covered in my last two postings. This is a basic solution for calling into Python from C++.
I've also included in the attached zipfile, notes on how to configure a Visual Studio 2008 solution to use Python26. I expect it is straightforward to translate these steps to work with a different compiler, or another version of Python. For completeness, these notes are included here:
Notes for creating the Project in Visual Studio 2008
Assumes Python26 is installed at C:\Python26, so be sure to update the paths listed below if you are using a different version of python.
Create a New Project --> Win32 Console Application, No precompiled Headers, Empty Project
Add main.cpp, pythonCallbacks.h, pythonCallbacks.cpp files to solution
Right click on PythonCppExample --> Properties
C/C++ --> General --> Additional Include Directories = C:\Python26\include
Linker --> General --> Additional Library Directories = C:\Python26\libs
(NOTE: Notice it is the libs directory, not the Lib directory)
Linker --> Input --> Additional Dependencies = python26.lib
It's been a while since I last posted. I'm managed to accomplish quite a few things since last journal entry. The most notable change to the code base was the completion of my animation channel implementation.
An animation channel can be though of as a joint overrider. It is used to merge a list of animation together into one seamless animation (e.g. combining a running animation with an animation of the model pointing at something will yield an animation of the model pointing and running at the same time).
The animation channel modification goes as follows. For each model instance, I have a list of animation channels. Each animation channel has an associated animation sequence (startFrame, endFrame, currentFrame) and loop boolean (channel removes itself from the list if the sequence completes and loop is set to false).
I currently have only 2 channels defined, BASE_CHANNEL and UPPER_BODY_CHANNEL. Adding new channels is straightforward in my codebase.
The way I define an animation channel is pretty simple. For each model, I maintain a map which associates a channel to a list of joint indexes. To load the UPPER_BODY_CHANNEL, i simply find the joint named "torso" (a required joint name for my models) and add it and all of its children to the list for the UPPER_BODY_CHANNEL. BASE_CHANNEL is simply a list of all the joints.
Aside from cleaning up various areas of the model code, I also fixed a visualization bug that was really bothering me for quite some time.
To have the texture splats showing up properly, I added a real tiny tweak value to tile location, so that the splats would be rendered slightly above the base tile, and thus would not be lost during depth testing. The result of this produced some crappy results depending on where the camera was located (see below).
Well, I finally used glPolygonOffset to remove this ugly artifact, and was left with some nice results.
No matter what angle the camera is at, it always looks that good.
I recently suffered a terrible HD crash on my coding laptop, which resulted in a loss of about 2 months of coding work. Needless to say, losing any amount of work that you are proud of certainly does suck. However, I've been able to collect a list of tasks that vanished as a result of this event and I should be back on track within a week or so.
There is something positive that came out of this crash, as I have been reminded of the importance of backup up my work. For a while I was doing pretty good, using a svn repository in addition to performing full archives of the code base with a zip file. That backup regiment has slowly tapered off, and lately I've been only zipping the code base without copying the zip up to a thumb drive or external.
In response, I've written the following script that I wanted to share that performs the backup for me. If you're really bad about backing up your work, you may want to do what I did: place the script directly on your external storage device and integrate it into the autorun. Directions on doing this are included in the scripts comments:
## file: backup.py
## author: Alex Wood
## date: 2008-06-04
## desc: Archives all files in each specified directory into a specific zip file.
## Place this script in the toplevel of a thumb drive or external harddrive
## to enable autobackup of directories, as described below.
## Copy and paste the following text into your autorun.inf text file:
## ACTION = Launch portable workspace
## Create launch.bat and add the following line:
## python backup.py
dirList = [
('C:\\foo', 'foo'), # directory, zip name
('C:\\bar', 'bar'), # directory, zip name
copyDest = os.getcwd()
numFiles = 0
for src in dirList:
currentPath = src
# verify the source directory exists
if not os.path.isdir(currentPath):
print '%s not found' % currentPath
# zipfile name is the concatenation of the zip name (defined in dirList) and a timestamp
zipname = src
zipfilename = "%s_%s.zip" % (zipname, datetime.datetime.now().strftime("%Y%m%d%H%M%S"))
file = zipfile.ZipFile(zipfilename , "w")
for (path, dirs, files) in os.walk(currentPath):
numFiles += len(files)
currentPath = path
for name in files:
archiveName = str(os.path.join(path, name))
# the archive name must be relative to the root of the archive
# so strip off the C:\foo part of the path
archiveName = archiveName[len(src):]
file.write(os.path.join(path, name), archiveName, zipfile.ZIP_DEFLATED)
print '%d files archived into %s' % (numFiles, zipfilename)
Welcome to Azrial's Development Journal. Azrial is an online role playing game that I have been developing on my own for quite some time. Once released to the public, this game will always remain free of charge. Throughout the evolution of Azrial's code base, I will be tracking my progress, ideas, and dicussions pertaining to Azrial here at this journal.
First, a little background on Azrial. The game initially started out as a 3D tile-based game. Only a 16x16 tiled map was rendered, and players could run off the map north, south, east, or west which would load a new map if one existed (if I can dig up some old screenshots, I'll post them here for context). The camera was essentially fixed, to keep the whole map into view. Players could only zoom in and out a little bit. There were major problems plaguing the game (organization was a mess, tons of memory leaks, slow performance).
After stepping back from the project, I wasn't happy with where Azrial was heading, so I decided to take all that I have learned from this experience, and rewrite the game. Below is a screenshot of where it currently stands:
The most drastic changes from the previous version, is that the realm is a continuous world. That is, all maps in view are rendered in their proper locations. In this particular screenshot, the player is actually standing on the corner, where 4 maps meet. Furthermore, the player has more control over the camera. Although the camera will always focus on the player's character, you can zoom, pan, and raise the camera via the mouse.
Just to go over some of the other features shown in this screenshot:
You can populate maps with 3d objects (such as the house and trees)
You can add a masking sprite to cover a tile (such as the flowers in front of the house)
You can add billboarded sprites to maps (the bush on the left side of the house)
There is a tile blending layer, where you can blend the texture from one tile into one of its N,S,E,W,NE,NW,SE,SW neighbors (the road blends outward into the grass)
This obviously not an exhaustive list of all that Azrial currently offers, but its a start.
I've been doing some serious simulation architecture designing and implementation for Azrial. This is a complete reworking on how simulation objects (actors, items, obstacles, areas, etc) are built and used. It's a lot of tedious upfront work, and providing little immediate visual reward. The reward comes in the satisfaction of knowing that this rework is making the simulation framework more generic and datadriven.
My simulation architecture is derived from Thor Alexander's simulation design illustrated in Game Programming Gems [GPG] (forget the number), and Massively Multiplayer Online Game Developement Book #1 [MMOGD] (I guess this makes me an [s]Addison Wesley [/s] Charles River Media fanboy -- but hey, that publisher releases some damn good dev books).
In a nutshell, all objects are subclassed from ISimulationObject. Simulation Objects (aka SOB) can contain other simulation objects (e.g. a player can contain a backpack -- backpack can contain several items, etc). Simulation Objects have links to other objects, such as an area will have links to other adjacent areas, party members may have links to other party members. These simulation objects also support the observer pattern, so those object interested in events for a particular SOB can subscribe to it and recieve update notifications.
When a client logs onto the server, the client is attached to an Actor instance (which is a subclass of simulation object). Actors are controlled by Actions. The Actions are modelled using the State Design Pattern. In particular, Actors make use of multiple parallel state machines. The state machines supported thus far include a TurningState machine (controls the orientation of the actor), MovementState machine, ActionState machine, and PostureState machine. Using parallel state machines, keeps the state machine design simple in terms of design and yields more flexible behavior. Using this pattern means that for an actor to change its state from standing and idle to running forward and casting a spell, the actor must be able to transition its movement state from standing to running forward and transition its action state from idle to spell casting. But enough about that, see [MMGD] for more detail.
On the client side, Sobs have corresponding proxy classes. So where we have a CActor on the server, we'll have a lightweight CActorProxy on the client. In attempts to keep simulation separated from the actual rendering, I make use of the Observer Pattern. When we have a SOB that requires rendering, I attach the appropriate observer (such as a RenderModelObserver) that is in charge of registering/unregistering the 3d model with the model manager.
As it stands right now, I can login, and receive my character's base model information from the server. I'm in the process of setting up how models attach to other models in a generic fashion (e.g., attaching a weapon to the hand of the character). This will hopefully be done tonight after work.
set up a good architecture for the joint attachments, and added some to a base character model. In the screenshot below, I attached a goatee, hair, shoulder armor, and the weapon. As you can see, I'm not the world's best 3d modeler. But it is good enough for now, and is easily replaceable in the future. The code to attach an accessory to a joint is straightforward and easily extendable, so I am ready to move on to the next task: Animation channels.
I have finished the UDP network communication layer between the client and the server and i'm pretty happy with how well it is working right now. I am properly handling reliable and unreliable packets, using the Reliable UDP protocol presented in Game Programming Gems 5.
The two components that I plan on adding next include a Network Simulator and an ecryption scheme. The network simulator is just a component that sits between my socket wrapper and the raw socket, and will be responsible for dropping packets, duplicating packets, and reordering packets. Each of these actions will be seeded with a probability, as a means of simulating particular network behavior. This will make it easier for me to catch problems with packet reliablity while developing, rather than encountering these problems later on.
I am still working out which encryption algorithm will best suit my needs. I just read something on slashdot about the one-time-pad , and that may be a good solution. If I do attempt this, perhaps I'll see how well it works when using a pseudo random number generator to generate the "pad". More on this later (hopefully).
Per suggestions by Sange (AKA Gospel) and Hope Dagger, I want to add some screenshots showing some progress. Unfortunately screenshots of my UDP protocol in action isn't all that exciting, so I whipped up a crappy helmet item. One thing I do love is how items attach to player skeletons effortlessly. This didn't take me long to whip up, so keep in mind that over time crappy artwork will be phased out as the game progresses.
As I integrate the last few utilities in to the network system, I am going to start hooking up a database to the server. At present time, I'm leaning towards using SQLite as my database system for several reasons: I like its liscense (public domain), it is lightweight and embedded. This will be my first project using a Database System, so I am not 100% sure if SQLite will meet my performance needs. For that reason, I want to be sure that I can swap out SQLite for alternative db software (e.g. postgre or mysql). Enter the Adapter Design Pattern:
There isn't much to this uml diagram -- i put it together just minutes before i had to leave for work. The purpose of the IDBAdapter will be to establish a standard interface to a database, allowing me to swap out SQLite if I want to experiment with another DB system. Naturally, the database formats won't be the same, but code wise the server shouldn't notice the difference.
I should have posted this earlier, but I needed a world of warcraft intermission this weekend.
The interface subsystem has been drafted. I have many of the basic components set up, including labels, panels, borders, textfields, buttons and chatboxs. There are more components that need to be added down the road (such as a scroll widget, sliders, progress bars, textured panels, etc), but most of the work is already done for these, as I tried to make my interface subsystem easily extendable.
The gui wasn't the only thing I have been working on since my last post. I encountered a few performance issues that I wanted to resolve before I finished the interface.
Aside from my memory leak error that I had a hard time tracking down, and my mipmap mishap, I had to rewrite my font manager.
Initially I was using bitmaped fonts, as I was under the impression that these were faster than your standard texturemapped fonts (I misread something in the www.opengl.org faq). I was horribly mistaken. It took me a bit to find out that this was where I was taking a performance hit, but once I found out that texture mapped fonts were the way to go, I was on my way.
Next on my list is adding texture splatting to my terrain.
Even though I had the week off after Xmas, I have been keeping myself busy. As a result, I have been working on minor changes whenever I get some free time. Some of you who have visited the dev journal prior to yesterday, may have noticed the new banner (created using The Gimp). It seemed like an easy enough task to accomplish while sitting in the airport terminal. I'm pretty new to The Gimp, but I'm liking the features and script-fu's that are provided (not to mention the fact that it is free).
Aside from a few minor bug fixes (character animation was horrible -- now it is just amatuer; some player navigation issues resolved), I added some new ini settings. Players can specify whether or not they want to run in fullscreen and players can specify the size of the mapcache. The mapcache (currently defaulted at 100) is simply the number of maps that you want to keep loaded in memory. This ini setting also allows the players to find a balance between performance and memory consumption. Even though the maps are fairly small in size, players may wish to crank this number up so that maps are only loaded once at startup (and never unloaded/reloaded while playing).
The next task on my list, is to setup some game events and prepare the system for my user interface subsystem.
I managed to get multitexturing working, and i'm pretty happy with the results. I now have an alpha map texture, which is used to "stamp" the new texture on top of the existing base tile. With the addition of texture splatting, the old system of blending tiles with their neighbors is thrown out the window, seeing that texture splatting can take care of this for me now. If you would like to use splatting as well (without shaders), I have included code to do this with or without the use of vertex arrays (see here).
In a nutshell, I am just replacing the alpha values of a texture with the alpha values in my selected alpha tile, and attaching that texture to a quad which is rendered over top of the base texture. An example:
Even though I was shooting for at least 1 new screenshot a week, I don't think it is worth it at this point in time. I'm at the point right now, were coding takes precedence over modelling and terrain building. So rather than show a screenie of the same old stuff, I figured I'd hold off until a few important features are built into the game. That said, I have been taking care of some important components of the server.
The server is now able to interface with an SQLite database. It took me a little bit of fiddling around to get the interface wrapper just the way I want it to work. Now that it is done, i'm very happy with it. Following this completion, I have started the players table, which will maintain persistent storage of player accounts. As it follows, I will be working on the new account/login routines shortly.
Since my last post, I have been spending my efforts on integrating game events into Azrial. So far, so good.
After completing the event subsystem, I moved onto writing a generic user interface system. I am aware of all of the libraries out there that already do this for you, but 90% of the fun of game development is doing to work yourself and learning from your own mistakes.
Anyway, so I decided to develop my interface as a heirarchical tree. I will describe the gui more in a later post, when I get a few more generic UI components written up (buttons, textfield, textarea).
The components that I did complete include:
-Borders (can be added to any component)
I was off to a great start, until I decided to make a panel that displays my current FPS. I was shocked to find out that my FPS was down to 40 (I had a bug in my original fps log file).
I went on an optimization kick, and brought the fps back up to 65. I still have more tricks up my sleeve, but it suffices for now.
Next post, I'll show off my new generic interface system with some screenshots, and perhaps some code snippets for the gamedev community.
Classic move on my part. Before I jump into Animation Channels as I said I would in my last post, I decided to fix a few things bugging me with my ModelMGR.
The way things currently work is that each model has a corresponding texture with the same name (obviously different extension, but you get the point). Well, this is obviously a crappy design, because if I wanted to change the skin for a model, I would need to duplicate that model and rename it to match that skin. Furthermore, I couldn't share textures across models (e.g., the goatee and hair textures are the exact same texture, but are duplicated at the moment). I've know that this is a problem, and its a good time to address it.
So once I have models specifying their texture, rather than using a predefined texture, I want to optimize the model rendering. To minimize the number of texture binds I will have, I need to sort the models by texture. My texture manager will ignore bind calls that try to bind a texture that is already bound, so this should save some overhead. This is my task for the weekend.
Before I dive into the networking layer, I wanted to do some refactoring on my model system. I'm off to a good start, and I am really excited about the direction I am taking with my 3d model format.
In previous screenshots, you probably have noticed that dude in the white shirt and brown pants. Model is a milkshape3d model that I load directly into Azrial. When transitioning from the Azrial prototype to its current incarnation, I made the executive decision to phase out m2d models in favor of a format that supports skeletal animation; bringing me to the ms3d format.
I am now just starting to really reap [some of] the benefits of skeletal animation. Prior to this post, my model format allowed for 1 child model, a weapon, to be attached to a specific joint, the main hand. Although the mainhand joint was located at loadtime, the whole joint updating for the child model was essentially hardcoded (only 1 child model allowed and only 1 joint was expected to be attached). I have recently generalized this process to allow for attaching multiple submodels, and linking multiple joints. So now, instead of just rendering a character model and a an optional weapon, I can render a base character model, with customized hair, armor, shield, etc.
This still requires more testing, but so far so good. My initial list of submodels I plan on adding are as follows:
offhand object (second weapon, held charm)
The next model modification I will be adding is the concept of animation channels, inspired by a reply from xEricx here (definite ++rating). With this, I will be able to render more versatile animations, such as swinging a weapon while running. I am currently designing how this will be done within Azrial, but once I have this working I'll be sure to post more screenshots!