Wreckage
Project Status Finished
Project Type Professional / group project
Project Duration ~2 years
Software Used CryEngine
Languages Used C++, Lua, CryEngine Flow Graph
Primary Role(s) Lead Programmer







Visit Wreckage on ModDBDownload Wreckage Standalone Version


Rainy Days and Wreckage are two full conversion modifications based on the videogame Crysis I worked on with a permanent team of 4. Spanning over 3 years of active work these 2 projects and an individual level release eventually formed a trilogy of strong plot-driven releases all set in the same storyline. As the Lead Gameplay Programmer I oversaw all phases from pre-production to quality assurance.

Wreckage especially was critically acclaimed, got several magazine covers and up to this day has been downloaded more than 400 000 times. Crytek, impressed with the product's fidelity, gave us permission to do a stand-alone release.

About Wreckage

Designed as the finale of the trilogy, Wreckage went through the longest development cycle given it being the most ambitious title we've made so far. After our prior release Rainy Days received praise and a great reception, we wanted to put the bar even higher. Longer playtime and more gameplay variety was the main goal. To this end I created new systems and modified code that firmly moved us away from the base CryEngine 2.

Pulling this kind of production off with a small team while having a polished final product free of bugs and issues proved to be one of the most challenging things I have worked on to date. Despite the looming deathlines, last minute fixes and community pressure, we delivered an end product that I'm immensely proud of to this day.

Gameplay Demo

Level 1 Gameplay Demo

While Wreckage has three levels the very first one went through the most iterations and thus was my main focus point for a lot of the production. The main level scripting was achieved using Lua and with the help of the visual Flow Graph system akin to the Blueprints found in Unreal Engine.


A big emphasis was placed on variety. Over the course of the roughly 60 minutes long level there is a on rails helicopter section, area defence, capturing a village, surviving an ambush, traversing a cave system, infiltrating a facility and finally an explosive finale where a command centre has to defended while multiple waves of enemies close in from all sides.

This level alone took almost a year to fully assemble and sports more than 13 minutes of fully German voiced dialog (or more than 2000 spoken words) which got subtitled in 3 languages including Spanish. All dialog was entirely written by us and recorded by professional voice actors. 

Level Transition System

To load in a new level the base CryEngine 2 always displays a separate fullscreen loading screen. While this works for the clear cut Wreckage has between Level 1 and 2, Level 2 and 3 are supposed to feel like a single long level so this interruption was bothersome. Therefore I modified the engine code to have a smoother transition:


I decided to capture the last framebuffer and statically display it while overlaying a subtle Flash animation while it loads. Furthermore the player state and position data is saved and applied to the next map allowing you to spawn in the exact same location where the loading trigger was hit. Clever level design does the rest to create a believable illusion.
 

CODE SNIPPET - Level Transition System

This code illustrates how the last framebuffer is captured and statically rendered every frame alongside the Flash animation:

void CFlashMenuObject::OnLoadingStart(ILevelInfo *pLevel)
{  
    if(gEnv->pSystem->IsEditor() || gEnv->pSystem->IsDedicated()) return;

    m_bInLoading = true;
    string levelName = pLevel->GetName();

    IGameTokenSystem *pGameTokenSystem = gEnv->pGame->GetIGameFramework()->GetIGameTokenSystem();

    string previousLevel;
    pGameTokenSystem->GetTokenValueAs("Game.General.Previous_Level", previousLevel);

    CryLogAlways("Wreckage: Previous Level: %s", previousLevel);

    if (previousLevel == "Wreckage_Part2" && levelName == "Wreckage_Part3")
    {
        CryLog("Wreckage: Start part 3 with smooth transition");        

        //Capture framebuffer
        gEnv->pRenderer->ScreenShot("Mods/Wreckage/Game/Levels/Wreckage_Part3/wreckage_03_loading_new.jpg", gEnv->pRenderer->GetWidth());

        LoadTransitionScreen(pLevel);

        CryLog("Wreckage: Loading part 3 initiated");
    }
}

/////////////////////////////////////////////////////////////////////////////
void CFlashMenuObject::LoadTransitionScreen(ILevelInfo *pLevel)
{
    if(pLevel)
    {
        m_iMaxProgress = pLevel->GetDefaultGameType()->cgfCount;
    }
    else
    {
        m_iMaxProgress = 100;
    }
 
    if(!m_apFlashMenuScreens[MENUSCREEN_FRONTENDLOADING_TRANSITION]->IsLoaded())
    {
        CryLog("Wreckage: Load transition flash file");

        m_apFlashMenuScreens[MENUSCREEN_FRONTENDLOADING_TRANSITION]->Load("Libs/UI/Menus_Loading_Wreckage_Part3.gfx");
        m_apFlashMenuScreens[MENUSCREEN_FRONTENDLOADING_TRANSITION]->GetFlashPlayer()->SetFSCommandHandler(>span class="br0">);
        m_apFlashMenuScreens[MENUSCREEN_FRONTENDLOADING_TRANSITION]->Invoke("SetProgress", 0.0f);

        UpdateMenuColor();
    }

    m_pCurrentFlashMenuScreen = m_apFlashMenuScreens[MENUSCREEN_FRONTENDLOADING_TRANSITION];

    //Prepare static loading texture
    IRenderer* pRenderer = gEnv->pRenderer;
    assert( pRenderer != NULL );

    if (pRenderer)
    {
        IUIDraw *m_pUiDraw = gEnv->pGame->GetIGameFramework()->GetIUIDraw();
        assert( m_pUiDraw != NULL );

        if (m_pUiDraw)
        {
            char fileName[255];
            sprintf(fileName,"Game/Levels/Wreckage_Part3/wreckage_03_loading_new.jpg");

            m_LoadingTexture = m_pUiDraw->CreateTexture(fileName);

            pRenderer->SetState( GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCALPHA | GS_NODEPTHTEST );

            m_bIsQuickLevelSwitchLoading = true;
        }
    }

    m_bUpdate = true;
    m_nBlackGraceFrames = 0;  

    //Remove gameplay HUD
    if(g_pGame)
        g_pGame->DestroyHUD(); 
}

/////////////////////////////////////////////////////////////////////////////
void CFlashMenuObject::OnPostUpdate(float fDeltaTime) //Called every frame just before the framebuffer is displayed
{
    //Wreckage MOD: Draw loading texture if needed
    if (m_bIsQuickLevelSwitchLoading && m_LoadingTexture != 0)
    {
        IRenderer* pRenderer = gEnv->pRenderer;
        assert( pRenderer != NULL );
     
        if (pRenderer)
        {
            pRenderer->Draw2dImage(0.0f,-0.5f, 800.0f,600.0f, m_LoadingTexture, 0.0f,1.0f,1.0f,0.0f, 0.0f);      
            
            if (m_bInLoading == false && m_nBlackGraceFrames == 0)
            {
                m_bIsQuickLevelSwitchLoading = false; //Loading has finished, stop rendering texture after this frame
            }
        }
     }
}
Dynamic Player Character Switch

In Wreckage we introduced a second playable character for the first time. Captain Lambert joins the team as an ordinary soldier who has to rely on taking cover and running away more often than the original NanoSuit equipped protagonist Nomad who was the only playable character in Rainy Days. 


While Nomad is still the avatar for the first level, afterwards the point of view switches to Lambert for Level 2 and 3. The engine was only coded to allow one character model for every level, so I added a new system command to switch between the two heroes: 


The command can be invoked from various places and affects the models, difficulty level, HUD, reaction dialogs and movement feel and balance.

 

CODE SNIPPET - Character Switch

When the loading of a map finishes the appropriate player for that level is set automatically:

void CFlashMenuObject::OnLoadingComplete(ILevel *pLevel)
{
    if(gEnv->pSystem->IsEditor() || gEnv->pSystem->IsDedicated()) 
        return;

    //Wreckage MOD: Load the correct player model and HUD based of level
    string levelName = pLevel->GetName();
    
    if (levelName == "Wreckage") //Level 1: Nomad
        gEnv->pConsole->ExecuteString("wreckage_suit_activate");
    else if (levelname == "Wreckage_Part2" || levelname == "Wreckage_Part3") //Level 2/3: Lambert
        gEnv->pConsole->ExecuteString("wreckage_suit_deactivate");
}


The function to switch from Nomad to Lambert. Can be invoked after a level finishes loading, by a custom Flow Graph node or by command line which was used during development in the editor:

//Wreckage MOD: Switch from Nomad-Lambert
void CGame::CmdDeactivateSuit(IConsoleCmdArgs* pArgs)
{
    CryLogAlways("Wreckage Log: NanoSuit disabled!");

    ICVar* suit = gEnv->pConsole->GetCVar("wreckage_isSuitActivated"); //this variable allows various other systems to check the current player model
    if(suit) 
            suit->ForceSet("0");

    //Change player arms and model to Lambert
    IScriptSystem* m_pScriptSystem = g_pGame->GetIGameFramework()->GetISystem()->GetIScriptSystem();

    CActor* pActor = static_cast<CActor*> (g_pGame->GetIGameFramework()->GetClientActor());
    if (pActor)
    {
        SmartScriptTable m_script = pActor->GetEntity()->GetScriptTable();
        if (m_pScriptSystem)
        {
            m_pScriptSystem->BeginCall(m_script, "SetModel"); m_pScriptSystem->PushFuncParam(m_script);
            m_pScriptSystem->PushFuncParam("objects/characters/human/us/nanosuit/nanosuit_us_multiplayer.cdf"); 
            m_pScriptSystem->PushFuncParam("objects/weapons/arms_lambert/arms_nanosuit_us.chr"); 
            m_pScriptSystem->PushFuncParam("objects/characters/human/asian/nk_soldier/nk_soldier_frozen_scatter.cgf"); 
            m_pScriptSystem->PushFuncParam("Characters/Lambert_fp3p.cdf"); 
            m_pScriptSystem->EndCall();

            pActor->Physicalize();
        }
        
        //Adjust STANCE_STAND speed values to give Lambert higher default walking/running speed
        CPlayer* pPlayer = static_cast<CPlayer*> (pActor);
        if (pPlayer)
        {
            const SStanceInfo *sInfo = pPlayer->GetStanceInfo(STANCE_STAND);
            SStanceInfo newStandInfo;

            //Copy old values since they remain the same
            newStandInfo.heightCollider = sInfo->heightCollider;
            newStandInfo.heightPivot = sInfo->heightPivot;

            newStandInfo.leanLeftViewOffset = sInfo->leanLeftViewOffset;
            newStandInfo.leanLeftWeaponOffset = sInfo->leanLeftWeaponOffset;
            newStandInfo.leanRightViewOffset = sInfo->leanRightViewOffset;
            newStandInfo.leanRightWeaponOffset = sInfo->leanRightWeaponOffset;

            newStandInfo.modelOffset = sInfo->modelOffset;
            strcpy(newStandInfo.name, sInfo->name);
            newStandInfo.size = sInfo->size;

            newStandInfo.useCapsule = sInfo->useCapsule;
            newStandInfo.viewOffset = sInfo->viewOffset;
            newStandInfo.weaponOffset = sInfo->weaponOffset;

            //Set new speed values
            newStandInfo.normalSpeed = 4;
            newStandInfo.maxSpeed = 9;

            pPlayer->SetupStance(STANCE_STAND, &newStandInfo);
        }
    }

    //Set suit mode to default for safety since it's no longer in use
    CPlayer *pPlayer = static_cast<CPlayer *>(g_pGame->GetIGameFramework()->GetClientActor());
    if(pPlayer)
    {
        CNanoSuit *m_pNanoSuit = pPlayer->GetNanoSuit();
        m_pNanoSuit->SetMode(NANOMODE_DEFENSE, true);
    }

    //This is only relevant when triggering a change mid level from the command line. Refresh the currently used player item so the new model is used
    if (gEnv->pSystem->IsEditor())
    {
        //Select player fists as default weapon
        if (pActor && pPlayer)
                pPlayer->SelectFists(true);
    }

    //Set custom difficulty level for Lambert
    g_pGame->GetMenu()->SetDifficulty(0);

    //Change the HUD
    g_pGame->GetHUD()->SetLambertHUDEnabled(true);
}


This function is used to re-select the player arms in case the player character is switched mid level so the new arm model gets used. Only relevant during development:

void CPlayer::SelectFists(bool active)
{
    if(active)
    {	
        COffHand *pOffHand = static_cast<COffHand*>(GetItemByClass(CItem::sOffHandClass));
        
        //Drop object or NPC if player has grabbed something
        if (pOffHand)
        {
            if(pOffHand->IsHoldingEntity())
            {
                //Force drop
                if(pOffHand->GetOffHandState() == eOHS_MELEE)
                {
                    pOffHand->GetScheduler()->Reset();
                    pOffHand->SetOffHandState(eOHS_HOLDING_OBJECT);
                }
                    
                pOffHand->OnAction(GetEntityId(),"use",eAAM_OnPress,0);
                pOffHand->OnAction(GetEntityId(),"use",eAAM_OnRelease,0);
            }
            else if(pOffHand->GetOffHandState() == eOHS_HOLDING_GRENADE)
            {
                pOffHand->FinishAction(eOHA_RESET);
            }
        }

        //Select fists as default weapon
        CFists *pFists = static_cast<CFists*>(GetItemByClass(CItem::sFistsClass));

        if(!pFists)
            return;
        
        CItem *currentItem = static_cast<CItem*>(GetCurrentItem());
        if(!currentItem)
        {
            //Player has no item selected...just select fists
            pFists->EnableAnimations(false);
            SelectItem(pFists->GetEntityId(), false);
            pFists->EnableAnimations(true);
        }
        else if(currentItem->GetEntityId() == pFists->GetEntityId())
        {
            //Fists already selected...refresh them
            pFists->Select(false);
            
            SelectItem(pFists->GetEntityId(),false);        
            GetInventory()->SetLastItem(pFists->GetEntityId());
            pFists->Select(true);
        }
        else
        {
            //Deselect currently used item and select fists after
            currentItem->Select(false);
            
            pFists->EnableAnimations(false);        
            SelectItem(pFists->GetEntityId(), false);
            pFists->EnableAnimations(true);
        }
    }
}


At last the function that handles switching the HUD:

void CHUD::SetLambertHUDEnabled(bool bEnabled)
{
    if (m_animPlayerStats.GetVisible())
    {
        if (bEnabled)
	{
	    //Custom Lambert HUD
	    m_animPlayerStats.Load("Libs/UI/HUD_Lambert.gfx", eFD_Right, eFAF_Visible | eFAF_ThisHandler);
	}
	else
	{
	    //Default Nomad HUD
	    m_animPlayerStats.Load("Libs/UI/HUD_AmmoHealthEnergySuit.gfx", eFD_Right, eFAF_Visible | eFAF_ThisHandler);
	    EnergyChanged(m_pNanoSuit->GetSuitEnergy());
	}
    }

    //Update Health
    CActor *pActor = static_cast<CActor *>(gEnv->pGame->GetIGameFramework()->GetClientActor());
    if (pActor)
    {
        float fHealth = (pActor->GetHealth() / (float)pActor->GetMaxHealth()) * 100.0f + 1.0f;
	m_animPlayerStats.Invoke("setHealth", (int)fHealth);
    }

    //Update FireMode
    m_animPlayerStats.Invoke("setFireMode", m_curFireMode);

    //Update Ammo
    SFlashVarValue args[7] = { 0, m_playerAmmo, m_playerClipSize, m_playerRestAmmo, m_iGrenadeAmmo, m_sGrenadeType, true };
    m_animPlayerStats.Invoke("setAmmo", args, 7);
}
Chapter Select Menu

To allow easy re-playability and to have a set point to go back to in case of a game crash or critical bug occurring I added a completely new chapter system into the Engine. Flow Graph nodes unlock them based on story progress inside the level, which makes them appear in this newly added UI sub menu:


If the player decides to load a chapter the corresponding level is loaded in fresh. Then the chapter name is sent to the level Flow Graph to spawn the player in the appropriate place and it sets all relevant progression flags instead of starting from the beginning of the level. 



Custom Flow Graph Nodes

To speed up development and allow new gameplay features while scripting the levels I wrote several custom C++ Flow Graph Nodes that either expose useful engine functions or add entirely new functionality:

These are:

  • Time:DelayEx
    • A timer that can be cancelled if needed
  • Game:ForcePlayerStance
    • Forcing the player into a specific stance (standing, crouching, prone)
  • Game:ChangePlayerStanceSpeed
    • Overrides the default walking/sprinting speed values for a specific stance
  • Game:ToogleGodMode
    • Makes the player invincible for critical story moments
  • AI:GetGroupCount
    • Gets the count of all AI actors that were set to belong into a specific group. Used for spawn waves
  • System:ForceSetCVar
    • Adjust command line options directly from within the game script
  • Inventory:RemoveWeaponFromEntity
    • Allows to remove an item/weapon from any entity, not just the player
  • CrysisFX:PostFXFlashBang
    • Blinding the player with a strong white screen flash
  • Vehicle:GetComponentDamage
    • Gets the damage value of a specific part of a vehicle like the tires or the rotor of a helicopter
  • HUD:FlashExtended
    • Exposes the ability to render Flash files directly on top of the game. Used for animated text in Wreckage
  • HUD:DisplayObjectiveOnScreen
    • Displays objective icons and text markers directly on top of a specific entity or location in the main viewport


CODE FILE - Flow Graph Node "Force Player Stance"

The following code file illustrates two of the new Flow Graph Nodes that I added into the CryEngine 2. The first can be used to force the player into a specific stance while the second one allows to set corresponding speed values:

/////////////////////////////////////////////////////////////////////////////
// FlowForcePlayerStance.cpp
//
// Purpose: Flow node for forcing player into a stance
//
/////////////////////////////////////////////////////////////////////////////
 
#include "StdAfx.h"
#include "Nodes/G2FlowBaseNode.h"
 
#include "Player.h"
#include "PlayerInput.h"
 
/////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////
class CFlowNode_ForcePlayerStance : public CFlowBaseNode
{
private:
    SActivationInfo m_actInfo;
 
public:
    enum EInputPorts
    {
        EIP_Trigger = 0,
        EIP_Stance,
    };
 
    enum EOutputPorts
    {
        EOP_Done = 0,
    };

    ////////////////////////////////////////////////////
    virtual ~CFlowNode_ForcePlayerStance(void)
    {
    }

    ////////////////////////////////////////////////////
    virtual void Serialize(SActivationInfo *pActInfo, TSerialize ser)
    {
        if (ser.IsReading())
            m_actInfo = *pActInfo;
    }
  
    ////////////////////////////////////////////////////
    virtual void GetConfiguration(SFlowNodeConfig& config)
    {
        static const SInputPortConfig inputs[] =
        {
            InputPortConfig_Void  ("Trigger", _HELP("Trigger")),
            InputPortConfig<int>( "Stance", -1, _HELP("Try to set Stance on Player"), 0, _UICONFIG("enum_int:Stand=0,Crouch=1,Prone=2")),
            {0}
        };
        static const SOutputPortConfig outputs[] =
        {
            OutputPortConfig_Void ("Done", _HELP("Set Stance Done")),
            {0}
        };
        config.pInputPorts = inputs;
        config.pOutputPorts = outputs;
        config.sDescription = _HELP("Tries to force the player into a stance");
        config.SetCategory(EFLN_APPROVED);
    }
 
    ////////////////////////////////////////////////////
    virtual void ProcessEvent(EFlowEvent event, SActivationInfo *pActInfo)
    {
        switch (event)
        {
            case eFE_Initialize:
            {
                m_actInfo = *pActInfo;
            }
            break;
 
            case eFE_Activate:
            {
                if (IsPortActive(pActInfo, EIP_Trigger))
                {
                    CActor *pPlayerActor = static_cast<CActor *>(gEnv->pGame->GetIGameFramework()->GetClientActor());
                    if (pPlayerActor)
                    {
                        CPlayer* pPlayer = static_cast<CPlayer*> (pPlayerActor);
                        if (pPlayer)
                        {
                            int desiredStance = GetPortInt(pActInfo, EIP_Stance);
   
                            IPlayerInput* pPlayerInput = pPlayer->GetPlayerInput();
                            if(pPlayerInput)
                                pPlayerInput->Reset();
 
                            pPlayer->SetStance((EStance) desiredStance);
           
                            IPhysicalEntity *pPhysEnt = pPlayer->GetEntity()->GetPhysics();
                            if (pPhysEnt)
                            {
                                const SStanceInfo *sInfo = pPlayer->GetStanceInfo((EStance) desiredStance);
 
                                pe_player_dimensions playerDim;
                                playerDim.heightEye = 0.0f;
                                playerDim.heightCollider = sInfo->heightCollider;
                                playerDim.sizeCollider = sInfo->size;
                                playerDim.heightPivot = sInfo->heightPivot;
                                playerDim.maxUnproj = max(0.0f,sInfo->heightPivot);
                                playerDim.bUseCapsule = sInfo->useCapsule;
 
                                pPhysEnt->SetParams(&playerDim);
                            }
 
                            pPlayer->StanceChanged((EStance) desiredStance);
 
                            //Request new animation stance
                            if (pPlayer->GetAnimatedCharacter() != NULL)
                                pPlayer->GetAnimatedCharacter()->RequestStance(desiredStance, pPlayer->GetStanceInfo((EStance) desiredStance)->name);
 
                            //Awake entity just in case
                            if (pPhysEnt)
                            {
                                pe_action_awake aa;
                                aa.bAwake = 1;
                                pPhysEnt->Action(&aa);
                            }
 
                            if(pPlayerInput && desiredStance == STANCE_PRONE)
                                pPlayerInput->OnAction("prone",eAAM_OnPress,1.0f);
                           else if(pPlayerInput && desiredStance == STANCE_CROUCH)
                                pPlayerInput->OnAction("crouch",eAAM_OnPress,1.0f);
 
                            ActivateOutput(pActInfo, EOP_Done, true);
                        }
                    }
                    ActivateOutput(pActInfo, EOP_Done, false);
                }
            }
            break;
        }
    }

    ////////////////////////////////////////////////////
    virtual void GetMemoryStatistics(ICrySizer *s)
    {
        s->Add(*this);
    }
 
    ////////////////////////////////////////////////////
    virtual IFlowNodePtr Clone(SActivationInfo *pActInfo)
    {
        return new CFlowNode_ForcePlayerStance();
    }
};

/////////////////////////////////////////////////////////////////////////////
// Purpose: Flow node for adjusting the speed values of a particular stance
/////////////////////////////////////////////////////////////////////////////
class CFlowNode_ChangeStanceSpeed : public CFlowBaseNode
{
private:
    SActivationInfo m_actInfo;
 
public:
    enum EInputPorts
    {
        EIP_Trigger = 0,
        EIP_Stance,
        EIP_NormalSpeed,
        EIP_MaxSpeed,
    };
 
    enum EOutputPorts
    {
        EOP_Done = 0,
    };
 
    ////////////////////////////////////////////////////
    virtual ~CFlowNode_ChangeStanceSpeed(void)
    {
    }

    ////////////////////////////////////////////////////
    virtual void Serialize(SActivationInfo *pActInfo, TSerialize ser)
    {
        if (ser.IsReading())
            m_actInfo = *pActInfo;
    }
 
    ////////////////////////////////////////////////////
    virtual void GetConfiguration(SFlowNodeConfig& config)
    {
        static const SInputPortConfig inputs[] =
        {
            InputPortConfig_Void  ("Trigger", _HELP("Trigger")),
            InputPortConfig<int>( "Stance", -1, _HELP("Player stance"), 0, _UICONFIG("enum_int:Stand=0,Crouch=1,Prone=2")),
            InputPortConfig<float>( "NormalSpeed", 0.0f, _HELP("Normal speed for the stance (walk)")),
            InputPortConfig<float>( "MaxSpeed", 0.0f, _HELP("Max speed for the stance (sprint)")),
            {0}
        };
        static const SOutputPortConfig outputs[] =
        {
            OutputPortConfig_Void ("Done", _HELP("Change stance speed Done")),
            {0}
        };
        config.pInputPorts = inputs;
        config.pOutputPorts = outputs;
        config.sDescription = _HELP("Tries to change the speed of a stance for the player");
        config.SetCategory(EFLN_APPROVED);
    }
 
    ////////////////////////////////////////////////////
    virtual void ProcessEvent(EFlowEvent event, SActivationInfo *pActInfo)
    {
        switch (event)
        {
            case eFE_Initialize:
            {
                m_actInfo = *pActInfo;
            }
            break;
 
            case eFE_Activate:
            {
                if (IsPortActive(pActInfo, EIP_Trigger))
                {
                    CActor *pPlayerActor = static_cast<CActor *>(gEnv->pGame->GetIGameFramework()->GetClientActor());
                    if (pPlayerActor)
                    {
                        CPlayer* pPlayer = static_cast<CPlayer*> (pPlayerActor);
                        if (pPlayer)
                        {
                            int targetStance = GetPortInt(pActInfo, EIP_Stance);
                            float targetNormalSpeed = GetPortFloat(pActInfo, EIP_NormalSpeed);
                            float targetMaxSpeed = GetPortFloat(pActInfo, EIP_MaxSpeed);
 
                            const SStanceInfo *sInfo = pPlayer->GetStanceInfo((EStance)targetStance);
                            SStanceInfo newStanceInfo;
 
                            //Copy old values that remain the same
                            newStanceInfo.heightCollider = sInfo->heightCollider;
                            newStanceInfo.heightPivot = sInfo->heightPivot;
 
                            newStanceInfo.leanLeftViewOffset = sInfo->leanLeftViewOffset;
                            newStanceInfo.leanLeftWeaponOffset = sInfo->leanLeftWeaponOffset;
                            newStanceInfo.leanRightViewOffset = sInfo->leanRightViewOffset;
                            newStanceInfo.leanRightWeaponOffset = sInfo->leanRightWeaponOffset;
 
                            newStanceInfo.modelOffset = sInfo->modelOffset;
                            strcpy(newStanceInfo.name, sInfo->name);
                            newStanceInfo.size = sInfo->size;
   
                            newStanceInfo.useCapsule = sInfo->useCapsule;
                            newStanceInfo.viewOffset = sInfo->viewOffset;
                            newStanceInfo.weaponOffset = sInfo->weaponOffset;
 
                            //Set new speed values
                            newStanceInfo.normalSpeed = targetNormalSpeed;
                            newStanceInfo.maxSpeed = targetMaxSpeed;
 
                            pPlayer->SetupStance((EStance)targetStance, &newStanceInfo);
 
                            ActivateOutput(pActInfo, EOP_Done, true);
                            break;
                        }
                    }
                    ActivateOutput(pActInfo, EOP_Done, false);
                }
            }
            break;
        }
    }

    ////////////////////////////////////////////////////
    virtual void GetMemoryStatistics(ICrySizer *s)
    {
        s->Add(*this);
    }
 
    ////////////////////////////////////////////////////
    virtual IFlowNodePtr Clone(SActivationInfo *pActInfo)
    {
        return new CFlowNode_ChangeStanceSpeed();
    }
};
 
////////////////////////////////////////////////////
////////////////////////////////////////////////////
REGISTER_FLOW_NODE("Game:ForcePlayerStance", CFlowNode_ForcePlayerStance);
REGISTER_FLOW_NODE("Game:ChangePlayerStanceSpeed", CFlowNode_ChangeStanceSpeed);
Subtitle Creator

With over 7000 spoken words total, subtitling Wreckage was no easy task. CryEngine already includes a robust system to create subtitles and add additional effects (such as static) to the audio in real time without having to alter the dialog files themselves. But the format wasn't well documented nor very user friendly to actually work with, so I created a very simple editor which the Lead Story Writer was able to use:


Effects and properties could be selected from dropdowns and sliders e.g. how much to fade all other audio while this dialog is playing with tooltip support explaining them.


Since the engine didn't handle line breaks automatically they had to be manually added into the subtitle as well as determining when to show an entire new chunk. To facilitate rapid subtitle creation I imported the actual flash file which was used to render the subtitles in the game into this tool so you were able to quickly preview how your subtitles and line breaks were going to appear in the real game later:


Entire batches of dialog could be handled within minutes while producing pleasing looking results that were matching the audio speed.

Development History

The shown feedback screen after exiting Wreckage


Production on Wreckage started very early on after Rainy Days was released and took us on a long journey. In this section you'll find a few snippets of how the development progressed.

Level 1 Phases:

Pre-Alpha

Alpha


Beta

Final

Prototype: Cherokee

Cherokee was the original vision for Wreckage but due technical issues, such as the terrain size being way too big for the CryEngine 2 to handle properly, we ultimately shelved it and it transitioned into the present day Wreckage.


The following video showcases some of the content that got cut during development as well as the transition from key scenes from the earliest stages to the final game:

Scene Comparison Beta-Final:





From real life to Wreckage


Black Hawk model created for Wreckage

Wreckage's backstory published as a PR piece (German)

The Facts in Numbers:

Included with Wreckage's post mortem


  • 195 Flow Graphs
    • 8070 Nodes and
    • 17659 connections between them
  • 91 Track Views (animated sequences)
    • 7723 moved objects
    • 19371 position/animation keys
  • 27 minutes of dialogue
    • 573 dialog files taking up 47.6 MB
    •  1 minute of dialog for every 2.2 minutes average Wreckage playtime
    • More dialog than 4 levels of Crysis 1 combined
  • 373 lipsync animation files
    • 17/27 minutes of Wreckage's dialog are facial animated
  • 21601 subtitle lines
  • 3625 added C++ code lines to CryEngine 2
  • 35085 placed entities in the Editor
  • 30799 vegetation objects
  • 6070 AI helper points
  • 506 TagPoints for special events

Reception

Our hard work on Wreckage and listening to the feedback we received from Rainy Days paid off in near universal acclaim. We got inquiries from several PC print magazines such as the German GameStar and PC Action and the Australian PC PowerPlay to include our Mod on their DVD and dedicate a section of an issue to Wreckage.

PC Action also reviewed the Mod and gave it an 'Excellent':


The reception from players was similarly enthusiastic which expressed itself in high download counts and positive reviews on Mod DB with an average rating of 9/10:


Here is what some of Wreckage's players had to say:









The creator of the Crysis franchise, Crytek, noticed our Mod as well and were so impressed they gave us permission in coordination with EA to do a stand alone release of Wreckage without Crysis included. This resulted in massive download numbers and even more positive reviews.

As a thank you to Crytek we sent them a letter and Wreckage burned on DVD all packaged up in a mock-up physical case: