Rainy Days
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.

Gameplay Demo

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.

Level Flythrough

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:

  1. Infiltrate the Korean command centre and expose a mole. It was up to the player to decide his approach with several ways leading to the building and the option to either sneak inside or fight your way in by force
  2. Reach a designated observation point and order a precise air strike to prevent your fellow comrades from running into an ambush
  3. Sneak into the farm without getting caught by the patrolling guards. Triggering the alarm state for more than 5 seconds would fail the mission
  4. Defend the farm from enemy waves approaching from all sides
  5. Escape through the rice fields since 2 enemy helicopters arrive you can't fight
  6. Meet up with the reinforcements and take out the 2 trailing helicopters
  7. Make your way onto an elevated position and provide sniper cover support for your squad while they advance through the rice fields
  8. Ambush a Korean convoy and take it over
  9. Rendezvous with the main forces in the central meadow
  10. Enter one of the tanks that got flown in and clear the entire meadow from enemies so your convoy can advance
  11. Fend off an ambush placed on the convoy
  12. Clear the rest of the meadow and capture the hotel area at the end of the map
  13. Finally mount the truck's weapon and defeat 3 helicopters while a NPC drives the vehicle around the course (on-rails section) which afterwards triggers the outro cutscene

Helicopter Flare Defence System

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.

CODE FILE - Flare Defence System

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:

Example: Bringing a Scene from Concept to Gameplay

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.

Development History

Early sketch of an area in Rainy Days


Script intensive! All Flow Graphs combined into one picture


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:

  • Adding a warning into the game which prompted the player to save and re-open the game when it was close to crashing by exceeding the limit
  • Splitting Rainy Days into two levels. While this decision didn't come easy, it had the additional bonus of allowing easier development due increased parallel work ability. The heightmap was kept identical between the two maps, each of the levels just had one half of the objects missing relieving the total RAM usage. The transition was performed at the most sensible point in the level already using an early version of my dynamic level transition system to hide the process


Development of Rainy Days (Alpha-Beta-Final):


In the end despite the split into two levels the game was still packed with objects and attention to detail:


Reception

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: