Introduction
Sinn is a platform-puzzle game developed by a team of seven students from the Master of Videogame Creation at UPF (Pompeu Fabra University). The development of the game lasted 1 year and it was made using a custom engine, using Visual Studio (C ++), DirectX, LUA, 3DMax, Fmod, PhysX, Sustance and Cal3D.
Team
The team is formed by seven people, three artists and four programmers. In addition to the main team we had the help of two musicians who made a wonderful soundtrack for our game. We also had two people who did the voices for Seele, Golem and Quetz.
The Game
The main mechanic of Sinn allow us to switch between two different worlds, the 3D world and the 2D world.
When we "project" control passes from the character (3D model) to his shadow. In this way we can go through places where there is no floor thanks to the shadow of other objects.
The main mechanic of Sinn allow us to switch between two different worlds, the 3D world and the 2D world.
When we "project" control passes from the character (3D model) to his shadow. In this way we can go through places where there is no floor thanks to the shadow of other objects.
The main mechanic is unique because when the character passes control from the body to the 2D world, some of the rules of the 3D world are maintained.
Due to this, when we have control in the 2D world we must take into account our physical body to overcome the different puzzles, and thanks to this we have many more possibilities of gameplay.
For example, thanks to our particular mechanics, we can move in depth allowing us to reach areas where we could not reach before.
The main mechanic is unique because when the character passes control from the body to the 2D world, some of the rules of the 3D world are maintained.
Due to this, when we have control in the 2D world we must take into account our physical body to overcome the different puzzles, and thanks to this we have many more possibilities of gameplay.
For example, thanks to our particular mechanics, we can move in depth allowing us to reach areas where we could not reach before.
My Work
Durante el desarrollo de Sinn al ser un pequeño equipo de siete personas todas las decisiones de diseño se tomaron entre todos. En la parte de programación, sin embargo, cada uno empezó dedicándose a una parte del sistema para no perder el tiempo en tomar decisiones. Aunque mas adelante acabamos ayudándonos entre nosotros por lo que todos tocamos parte del trabajo de los demás.
Así pues, mi trabajo consistió principalmente en:
Artificial Intelligence
In Sinn the guardians are the enemies of the character and have only one mission, to prevent Seele from advancing. Therefore their behavior is based mainly on running after Seele and trying to catch them. The way it has to detect Seele is by sight with a limited range and the noises that the character can also make in a limited range around them.
The development of these behaviors have been done with the following behavior tree Behaviour Tree.
In this Behavior Tree you can see a multitude of nodes that correspond to the different behaviors, but in summary our enemies have six different types of behaviors. patrol, alert, suspicious, pursuer, combat and surrender.
Mas detalles
- Patrol
In this behavior, the guardian only moves between the different points of his patrol through the navmesh. Once he reaches a point he makes a short stop to look around and resumes his march.
- Alert
When a guardian has the alert level it will only look around in case the character is nearby.
- Suspicious
If the guardian enters this state it means that he has heard the character at some point close to him, so his way of acting will be to walk towards the area where he thinks he has heard something and look around. There is also the possibility that the character used the distraction system by throwing something that makes the guardian suspicious.
- Pursuer
This state can be reached for two reasons:
- By eye contact within range
- Due to the accumulation of suspicious events that indicate that the character must be close
In this state the guardian will run towards the character or the point where he thinks he may be (if he has only heard it but does not see it)
- Combat
This state occurs the moment the guardian has come close enough to the character to be able to attack him. But before attacking him he has to evaluate whether or not he can do it.
- Wait:
If he enters this node, it means that another guardian has been assigned to capture the character. This assignment occurs by distance to the character, so it will change the guardian that is closest to it. To do this, a BlackBoard is used where they store the distance to the character, which guardian has been assigned and other relevant information.
- Combat - Cliff:
This behavior is activated if near the area where we are caught there is a cliff where we can be thrown. Due to lack of time, it is disabled due to lack of animations.
- Combat - Constrict
When this behavior is activated, the guardian traps the character and he must press a button repeatedly to free himself.- Release:
This behavior occurs when the character presses the button enough times to break free, then the guardian releases the character and he falls to the ground. After a few seconds he will wake up confused and look around - Death:
If the character could not be released, the screen will go dark and the game will restart from the last point saved.
- Release:
- Wait:
- Surrender
The guardian will enter this state when the impossible path system is activated. This means that even though the character is in front of him or very close, the navmesh is unable to find a way.
To see all the artificial intelligence code of the guardians here is the link Guardian code repository
On the other hand, to synchronize the different behaviors with their corresponding animations, a finite-state machine was used that allowed the entire process to do in a more secure way.
Cameras
During the development of Sinn, one of my teammates who had started developing the cameras had problems with them. Because of this, he handed over to me the responsibility for them and taking his basic work, I rebuilt them.
In the game have many of different cameras that you can see in the previous videos, but in summary we have created four type of different cameras:
- 3D Cameras
- 2D Camera
- Fixed Cameras
- Rail Cameras
Mas detalles
Each type of camera has a different operation.
- 3D Cameras
These cameras work by two different parameters, the pitch and the yaw. The pitch parameter is obtained from the vertical movement of the mouse and oscillates between 0 and 1. With this parameter and having a curve that begins at the feet of the character and ends at the head, it is evaluated in which position of the curve we are and it is created the vertical movement of our camera in three dimensions.
The yaw parameter is obtained with the horizontal movement of the mouse. This parameter controls the rotation of the camera around the subject, which is generally the character.
- 2D Camera
This camera is used with the main mechanics. Because we wanted to simulate a 2D environment, the movement of this is only horizontal and vertical. The camera will center its subject in the shadow, although the parameters are fixed to try that at no time the body leaves the camera. On the other hand, to give greater importance to the shadow on the body, this camera has depth of field (DOF).
- Rail Cameras
These cameras use the same curves as 3D cameras, the difference is that the position on the curve is not evaluated by the pitch value, but by the position of the character with respect to this curve. Thus, and with a small margin, we positioned the camera at a point where you can see the character in third person
The peculiarity of these cameras is that they cannot be controlled by the player, except when we moving the character along a path.
To see the code of the cameras here is the link Cameras code repository
Character Movement
Although the basic movement of the character started as the responsibility of another of my teammates, because his approach did not quite work, I took responsibility for it. So, taking his work as a base, he remade the character's way of moving.
Mas detalles
- Walk, stealth and sprint
The operation of the character's movement is quite simple. For the movements of walking, sprinting and stealth, the moment in which the movement input was pressed is collected and using a simple algorithm we calculate the percentage of the maximum speed.
CEntity* ent_player = ctx.getOwner();
TCompPlayer* comp_player = ent_player->get();
float inc_curve = (1.0 / 10.0);
float tacc = stateData.time_acceleration;
float tnow = comp_player->getPlayerTimer() - comp_player->getIniTimeAcc();
float v100 = 1.0 - 1.0 / (tacc / inc_curve + 1.0);
float porAcc = 2.0 - v100 - 1.0 / (tnow / inc_curve + 1.0);
if (porAcc > 1) porAcc = 1;
return porAcc;
And in the same way, the moment in which the movement input was released is calculated and the speed is calculated until reaching the stop.
CEntity* ent_player = ctx.getOwner();
TCompPlayer* comp_player = ent_player->get();
float inc_curve = 1;
if(!comp_player->isJumping()) inc_curve = (1.0 / 50.0);
float tdec = stateData.time_deceleration;
float tnow = comp_player->getPlayerTimer() - comp_player->getIniTimeDec();
float v100 = 1.0 - 1.0 / (tdec / inc_curve + 1.0);
float porDec = (2.0 - v100 - 1.0 - inc_curve / (tnow + inc_curve )) * - 1.0;
if (porDec < 0) porDec = 0;
return porDec;
Because our third-person cameras have the character positioned slightly to the right to have a more plane of vision of the environment, and since the direction of the movement of the character is done through the front of the camera that we are using, I was presented with a small trouble. When we walked straight, little by little the character was twisting, greatly worsening the character's control.
Initially, an attempt was made to fix it through some complex calculations, but the result was still unsatisfactory. Thus, the final result was to create a direction component, which had communication with the current camera to know what yaw and pitch parameters had in each frame, but that unlike this, there would not be a slight deviation to the left to have the character on the right. In this way the direction problem was solved.
- Jump
For the jump it is somewhat more complicated since we wanted an adjustable jump in maximum height, jump time and length. For this reason we first separate the horizontal and vertical movement. For horizontal movement we use the same calculations as those we used when walking but with different maximum speeds. On the other hand, for the vertical movement a small algorithm of a parabola was calculated in which by introducing the maximum height and the maximum jump time, we calculated the trajectory in the air.
Also, as we wanted to give the jump more precision, two features were added.
The first is that horizontal movement does not have any type of impulse, so if we stop pressing forward, or change to move to the opposite side, the character will stop moving horizontally or change direction without any type of restriction. This causes the player to have a better sense of control when jumping.
The second characteristic is that the player can make a small jump or a jump to the maximum height. To do this, if the player releases the button at the beginning, the maximum height will be calculated with respect to the time the button was held when jumping began. This way you won't have to jump almost 2 meters to reach an obstacle at 20 cm.
With these characteristics, I divided the jump calculation process into two parts. In the first, the shape of the parabola is pre-calculated depending on the force of the jump (time it takes to press the jump button) and it also begins to jump, since we did not want the character to stay in place while we were running nailed.
In the second part, having the values of the algorithm pre-calculated with the force of the jump that the player pressed, it is calculated which is the altitude speed. With this value we will add it to the calculated horizontal speed, and we will be able to move our character while we jump.
//Precalc
float t = time_jump * jump_strength;
y0 = max_y_jump * jump_strength;
a = (2*2) (y0 / t*t);
float aux = j_y0 / j_a;
x0 = pow(aux, 1.0 / 2);
//Jump calc
float x = timer_jump;
float aux = x - x0;
float newY = -a * (aux*aux) + y0;
newY += y_ini_value;
- Movement over platform
When we make a movement on a platform that in turn moves, we cannot leave it to physx to do the calculations to move the character, since what we get is either to fall if is a horizontal movement or an incessant tapping of the platform with the character. Thus, in our case, what is done is that in each frame the platform communicates with the character to send waht movement it'll do. With this information, the character'll add it to his movement vector and the illusion will be created that the character (if he is stopped) is still on the platform and this is carrying him.
To see the character movement code here is the link Character movement code repository
Animations
During the development of Sinn I was in charge of integrating into the game all the animations that the artists created for the different characters as well as some objects.
The integration process in a game with its own engine means that you have to even touch the animations themselves, in this case from 3DMax. So although not at a professional level, I have acquired a lot of experience in this field.
Although Cal3D was introduced in the project, which is responsible for allowing us to reproduce animations, to automate the process and not need to write many lines of code each time an animation was wanted to reproduce, I created some classes that handled the management of animations, doing for example that when playing a new animation the previous one will be eliminated, blending between animations that had it configured this way, stopping animations at the moment in which it is indicated, etc.
In this section I will not upload videos because just by watching the videos of the game you can see the integration of the animations. To see the written code, here is the link Animations code repository
NavMesh
Navigation meshes were used for the movement of the guardians through the different maps of the game. Because guardians were my main job, it was my job to integrate them into the game. For this use the work of Mikko Mononen from which I obtained the necessary tools to create the mesh and then use it.
To create it I used the demo in which I loaded the 3dmax file with the stage.
For the integration of the navigation meshes, I needed to create a module that would act as an intermediary between Mononen's work and our engine. In this module I included those tools that I needed such as calculating the path between two points, calculating the closest point of a specific area of the mesh or launching a raycast.
See Code
#include "mcv_platform.h"
#include "module_nav_mesh.h"
void CModuleNavMesh::start()
{
navmesh = CNavmesh();
navmesh.loadMesh("data/navmeshes/milestone5.bin");
if (navmesh.m_navMesh) {
navmesh.prepareQueries();
}
else {
fatal("Error when creating navmesh\n");
}
}
void CModuleNavMesh::stop()
{
navmesh.destroy();
}
void CModuleNavMesh::renderDebug()
{
}
void CModuleNavMesh::update(float elapsed)
{
}
void CModuleNavMesh::renderInMenu()
{
}
vector CModuleNavMesh::calculePath(VEC3 pos, VEC3 dest, float step, float slop)
{
return navmeshQuery.findPath(pos, dest, step, slop);
}
vector CModuleNavMesh::calculePath(VEC3 pos, VEC3 dest)
{
return navmeshQuery.findPath(pos, dest, step_size, slope);
}
float CModuleNavMesh::wallDistance(VEC3 pos)
{
return navmeshQuery.wallDistance(pos);
}
bool CModuleNavMesh::raycast(VEC3 start, VEC3 end, VEC3& hitPos)
{
return navmeshQuery.raycast(start, end, hitPos);
}
VEC3 CModuleNavMesh::findNearestNavPoint(VEC3 start)
{
return navmeshQuery.closestNavmeshPoint(start);
}
VEC3 CModuleNavMesh::findNearestPointFilterPoly(VEC3 pos, PolyFlags filter)
{
return navmeshQuery.nearestPointFilterPoly(pos, filter);
}
VEC3 CModuleNavMesh::findRandomPointAroundCircle(VEC3 pos, float maxDist)
{
return navmeshQuery.findRandomPointAroundCircle(pos, maxDist);
}
VEC3 CModuleNavMesh::findRandomPoint()
{
return navmeshQuery.findRandomPoint();
}
Others
In addition to the previous work in Sinn I also did the following jobs:
- Objects with movement
For Sinn I developed a type of object to which you could include the type of movement that we wanted it to have, how long that movement would last and if the movement is a loop. The particular thing about these objects is that they do not move with animations, but with physics.
- Objects with animations
Another component that I developed were objects that had animations and could be activated at a specific time. In addition to activating the animation, they transfer the animation movement to the collider so that the movement is not only graphic, but also physical.
- Final event component
This component creates a list of events that occur in a certain order and time. These events can be any command that has been programmed into the game, from creating objects, shaking the camera, playing a sound and many others.
- Bone Tracker
This component allows any entity in the game to follow the same movement as the skeleton bone of another entity.