Project Status | Finished |
Project Type | Professional / group project |
Project Duration | 1 year and 9 months |
Software Used | CryEngine |
Languages Used | C++, CryEngine Flow Graph |
Primary Role(s) | Lead Gameplay Programmer |
Rainy Days was our first fully fledged Mod that we produced. A little rough around the edges in regards to technical issues - something we drastically improved upon for the follow up Wreckage. Nonetheless it was very ambitious for its time featuring one gigantic level that takes around 2 hours to complete.
At the beginning of development the team only consisted of our leader Christopher and myself so I was not only responsible for creating the entire gameplay, but also to manage the project and handle other aspects such as bouncing ideas around for the story.
Rainy Days was conceptualized as one big playable level from the beginning aiming to rival those found in the original Crysis game in size. We aimed high and by doing so hit the absolute limit of the CryEngine 2. Since we were operating under a tight schedule not every planned feature made it into the final game and sometimes we had to take shortcuts.
Nevertheless the end result was still pretty good to play with a healthy mix in gameplay variety:
We also stretched the Flow Graph system to the absolute maximum by trying to script new gameplay types the engine didn't have any native support for.
The following video shows the entire playable space of Rainy Days. The terrain measures 4096x4096 metres which is equivalent to 16.78 km².
The main objectives the player had to complete during the course of Rainy Days:
After an intense escape through the rice fields several helicopters attack the squad at once and you are tasked with taking them out. To make this fight more difficult and not as straightforward as simply shooting them all down back to back I came up with a flare system the helicopters would use to destroy incoming missiles for a while forcing the player to mix up his strategy.
The actual defence system was scripted in Flow Graph while I created two new Nodes in C++ to be able to realize it. One of them allows to search a box with a given range for live projectiles and returns the first one it finds. The second corresponding Node forces the selected projectile to explode immediately.
The code for the new Flow Graph Nodes:
///////////////////////////////////////////////////////////////// // Copyright (C), Marwin Misselhorn // FGPlugin Source File // // FlowProjectile.cpp // // Purpose: FG node to check for projectiles in a given area // ///////////////////////////////////////////////////////////////// #include "StdAfx.h" #include "Nodes/G2FlowBaseNode.h" #include "IEntitySystem.h" #include "WeaponSystem.h" #include "Projectile.h" #include "IItemSystem.h" #include "Weapon.h" ///////////////////////////////////////////////////////////////// class CFlowNode_CheckForProjectiles : public CFlowBaseNode { public: enum EInputPorts { EIP_Trigger = 0, EIP_TargetId, EIP_AmmoClass, EIP_Range, }; enum EOutputPorts { EOP_ProjectileId = 0, EOP_NothingFound, }; //////////////////////////////////////////////////// virtual ~CFlowNode_CheckForProjectiles(void) { } //////////////////////////////////////////////////// virtual void Serialize(SActivationInfo *pActInfo, TSerialize ser) { } //////////////////////////////////////////////////// virtual void GetConfiguration(SFlowNodeConfig& config) { static const SInputPortConfig inputs[] = { InputPortConfig_Void("Trigger", _HELP("Triggers the projectile check")), InputPortConfig<EntityId>("TargetId", _HELP("The center of the searchbox")), InputPortConfig<string> ("Ammo", _HELP("When set, limit projectile check to this ammo type"), 0, _UICONFIG("enum_global:ammos")), InputPortConfig<float>("Range", _HELP("Range for searchbox")), {0} }; static const SOutputPortConfig outputs[] = { OutputPortConfig<EntityId>("ProjectileId", _HELP("Called when the first projectile is found and returns the Id")), OutputPortConfig_Void("NothingFound", _HELP("Called when no projectile was found in the whole area")), {0} }; config.pInputPorts = inputs; config.pOutputPorts = outputs; config.sDescription = _HELP("Checks for projectiles in the selected area"); config.SetCategory(EFLN_ADVANCED); } //////////////////////////////////////////////////// virtual void ProcessEvent(EFlowEvent event, SActivationInfo *pActInfo) { switch (event) { case eFE_Activate: { if (IsPortActive(pActInfo, EIP_Trigger)) { CGame* pGame = static_cast<CGame*>(g_pGame); if (pGame) { //Get properties EntityId targetId( GetPortEntityId(pActInfo, EIP_TargetId )); string ammoClass( GetPortString(pActInfo, EIP_AmmoClass )); float range( GetPortFloat(pActInfo, EIP_Range )); IEntitySystem* pEntitySys = gEnv->pEntitySystem; IEntityClass* pAmmoClass = pEntitySys->GetClassRegistry()->FindClass(ammoClass); Vec3 pos = pEntitySys->GetEntity(targetId)->GetWorldPos(); //Create bounding box to check for projectiles SProjectileQuery pquery; pquery.box = AABB(Vec3(pos.x - range, pos.y - range, pos.z - range), Vec3(pos.x + range, pos.y + range, pos.z + range)); CWeaponSystem *pWeaponSystem = pGame->GetWeaponSystem(); int count = pWeaponSystem->QueryProjectiles(pquery); CProjectile* pFirstFound; for (int i = 0; i < pquery.nCount; ++i) { IEntity *pEntity = pquery.pResults[i]; if (!pEntity) continue; if(pEntity->GetClass() != pAmmoClass) //Ignore projectile if it's not in the Ammo Class the user specified continue; CProjectile* p = pWeaponSystem->GetProjectile(pEntity->GetId()); if(!p) continue; pFirstFound = p; } if(!pFirstFound || pFirstFound->GetEntityId() == 0) ActivateOutput(pActInfo, EOP_NothingFound, true); else ActivateOutput(pActInfo, EOP_ProjectileId, pFirstFound->GetEntityId()); } } } break; } } //////////////////////////////////////////////////// virtual void GetMemoryStatistics(ICrySizer *s) { s->Add(*this); } //////////////////////////////////////////////////// virtual IFlowNodePtr Clone(SActivationInfo *pActInfo) { return new CFlowNode_CheckForProjectiles(); } }; ///////////////////////////////////////////////////////////////// // Purpose: FG node for destroying a found projectile ///////////////////////////////////////////////////////////////// class CFlowNode_DestroyProjectile : public CFlowBaseNode { public: enum EInputPorts { EIP_Trigger = 0, EIP_ProjectileId, }; enum EOutputPorts { EOP_Succeeded = 0, EOP_Failed, }; //////////////////////////////////////////////////// virtual ~CFlowNode_DestroyProjectile(void) { } //////////////////////////////////////////////////// virtual void Serialize(SActivationInfo *pActInfo, TSerialize ser) { } //////////////////////////////////////////////////// virtual void GetConfiguration(SFlowNodeConfig& config) { static const SInputPortConfig inputs[] = { InputPortConfig_Void("Trigger", _HELP("Destroys the projectile")), InputPortConfig<EntityId>("ProjectileId", _HELP("Id of the projectile")), {0} }; static const SOutputPortConfig outputs[] = { OutputPortConfig_Void("Succeeded", _HELP("Called when the job is done")), OutputPortConfig_Void("Failed", _HELP("Called when projectile fails to destroy")), {0} }; config.pInputPorts = inputs; config.pOutputPorts = outputs; config.sDescription = _HELP("Destroys a projectile on call"); config.SetCategory(EFLN_ADVANCED); } //////////////////////////////////////////////////// virtual void ProcessEvent(EFlowEvent event, SActivationInfo *pActInfo) { switch (event) { case eFE_Activate: { if (IsPortActive(pActInfo, EIP_Trigger)) { CGame* pGame = static_cast<CGame*>(g_pGame); if (pGame) { //Get properties EntityId projectileId(GetPortEntityId(pActInfo, EIP_ProjectileId)); CProjectile* p = pGame->GetWeaponSystem()->GetProjectile(projectileId); if(!p) { ActivateOutput(pActInfo, EOP_Failed, true); } else { //Try to destroy the projectile p->Explode(true, true); ActivateOutput(pActInfo, EOP_Succeeded, true); } } } } break; } } //////////////////////////////////////////////////// virtual void GetMemoryStatistics(ICrySizer *s) { s->Add(*this); } //////////////////////////////////////////////////// virtual IFlowNodePtr Clone(SActivationInfo *pActInfo) { return new CFlowNode_DestroyProjectile(); } }; //////////////////////////////////////////////////// //////////////////////////////////////////////////// REGISTER_FLOW_NODE("Projectile:CheckForProjectiles", CFlowNode_CheckForProjectiles); REGISTER_FLOW_NODE("Projectile:DestroyProjectile", CFlowNode_DestroyProjectile);
The Node implemented into the defence system for the final game:
Originally we were planning to have a different more calm ending to Rainy Days. But after some iterations and lengthy discussions we ultimately decided it would be for the best to end the game on a high adrenaline note. So we came up with this scene:
The team would get attacked by approaching helicopters again that split up from an even bigger group. The player was then asked to mount the gun on the big convoy truck and defeat them while Major Cafferty drives the truck around the hotel area. The layout of this area already existed naturally which made it a good fit for this kind of gameplay.
As can be seen in the concept screen, after entering the truck at the meeting point Major Cafferty would drive the truck for a bit until realizing he hit a dead end which results in an amusing dialog moment. Afterwards he enters a continuous loop around the course.
The path is spiked with several checkpoints where he can stop and change course in case the player defeated all helicopters which breaks the loop and makes the truck head straight to the end as indicated by the second red line. Random dialog triggers on the path and enemy spawns in various places distract the player from the fact he is driving in circles and keeps his attention on the goal.
Here is the final assembled scene in action:
One big problem was imposed by the limited amount you can rotate the gun which results in blind spots where the helicopters can just continuously fire at you and inflict massive damage without you being able to do anything which could lead to frustration.
A subtle system was implemented that repairs the truck slowly and removes some of the damage again to avoid instant kills:
The biggest challenge by far was to make the helicopters behave since they had the tendency to crash in the mountains or trees. Defining strict fly zones for them reduced the problem, but didn't fix it entirely. So I opted to make them invulnerable unless the player had just fired a projectile at them that might hit. My self-written Flow Graph Node "CheckForProjectiles" came in handy here in an unexpected way:
But this caused another "bug" that repeatedly occurred in playtesting. When the player managed to destroy one of the rotors it would cause the helicopter to crash, but not explode upon impact since the script already had it set to invulnerable again. Unless the actual hull explodes it won't count the vehicle as being destroyed and thus it also doesn't trigger the condition to advance the level.
Since the heli could crash-land way off the path if the player hit it from far away he wouldn't have any way of destroying it once it hits the ground. This results in infinite laps around the course which is effectively a softlock.
To fix this bug I wrote another new Flow Graph Node in C++ that allowed the level script to check for a vehicle's component damage instead of the overall damage:
If one of the rotors got destroyed the level script would catch it now and permanently disable the invincibility system allowing the helicopter to explode upon impact. This ensured that you could always beat the objective without getting stuck.
Given Rainy Days was our first large scale production they were several bumps along the road. The most impactful one came in the realization towards end of development that areas became so packed the game would threaten to exceed the 1200 MB RAM limit imposed on 32 bit processes. While CryEngine 2 already supported 64 bit fixing this issue, limiting the player base in this way at the time simply wasn't acceptable. Two additions were made to alleviate this problem:
In the end despite the split into two levels the game was still packed with objects and attention to detail:
A few technical issues still in the game notwithstanding, Rainy Days was already met with a very positive reaction from players worldwide. This was in great part due our production values and high commitment to detail in every department.
One major positive influence was the offer by the original German voice actor of Nomad in Crysis 1 to reprise his role for our Mod. His professional voice acting for our protagonist added a great deal of credibility to the project. The supporting cast helmed by 2day Productions was no slouch either.
Receiving a wide release on several Crysis and modding related sites Rainy Days found success quick and holds a 8.5/10 rating on Mod DB as of this day:
Since Rainy Days was tied to the base Crysis 1 game the amount of players, while good for a Mod, never reached their true potential until we made Wreckage. Written reviews are mostly found in German communities, but some found their way onto Mod DB regardless: