Jump to content

reepblue

Developers
  • Posts

    2,504
  • Joined

  • Last visited

Blog Entries posted by reepblue

  1. reepblue

    Cyclone
    Back in the summer of 2017, I started experimenting with the idea of a puzzle game by placing launch pads on surfaces. The first build was very sloppy, and there wasn't any wall launching at this time. This build however was the first step of the experimenting process. Only one map was ever made and I just reused old Vectronic assets. 

     
    I shelved this concept until Winter of 2020 asI developed an itch to work on something and I wanted to get familiar with GIMP. This build again only had one main map along with other sample ideas but it was just another demo. 
    Cyclone Prototype - Work in Progress - Ultra Engine Community - Game Engine for VR
    Although I really liked the direction of the project, I didn't develop further on that iteration after that March, but I did show it to people to get thoughts which were mostly positive. At this time, I wanted to wait until the Ultra Engine shipped to revisit the concept yet again, but eventually I got bored of waiting and figured I'd have to spend a lot of time learning the new systems Instead of using the almost seven years of working with Leadwerks. The plan was to get a small game out within a year. I set my scope to be a simple Portal clone with 10 maps. Anything else I probably would have got overwhelmed and bailed out like I did many times before with other projects.
    I first started working on the foundation code base. I came up with the idea of a Stage Class with it handling all the Actors. The Stage class handles time, map loading, transitions, temp decals, and is responsible for attaching actors to the entitles in the editor via a Lua script. It's also is in-charge of ensuring that a Camera is present at all times. After every map load, a camera is created in which something like a Player actor would point to as it's Camera. It all works very nicely. 
    //========= Copyright Reep Softworks, All rights reserved. ============// // // Purpose: // //=====================================================================// #ifndef STAGE_H #define STAGE_H #if defined( _WIN32 ) #pragma once #endif #include "pch.h" enum StageEvent { /// <summary> // NULL; Use this for the main menu. This is the default. /// </summary> STAGE_EVENTNULL = 0, /// <summary> // Intermission; the event(s) between plays. /// </summary> STAGE_EVENTINTERMISSION, /// <summary> // Transition; Intermission between scenes. /// </summary> STAGE_EVENTTRANSITION, /// <summary> // Play; In-Game /// </summary> STAGE_EVENTPLAY }; struct TransitionData { std::string szMapPath; std::string szLandmarkName; Leadwerks::Entity* pLandmarkEntity; Leadwerks::Vec3 vLandmarkDistance; Leadwerks::Vec3 vCamRot; Leadwerks::Quat qCamQuat; Leadwerks::Vec3 vVelo; void Clear() { szMapPath = ""; szLandmarkName = ""; vLandmarkDistance = Leadwerks::Vec3(); vCamRot = Leadwerks::Vec3(); qCamQuat = Leadwerks::Quat(); vVelo = Leadwerks::Vec3(); pLandmarkEntity = NULL; } }; class StageActor; class GameMenu; class Stage : public Leadwerks::Object { protected: Leadwerks::World* m_pWorld; Leadwerks::Camera* m_pCamera; Leadwerks::Context* m_pFramebuffer; GameMenu* m_pMenu; StageEvent m_hEvent; bool m_bVSync; int m_intFrameLimit; std::string m_szCurrentSceneName; std::string m_szPreviousSceneName; //std::string m_szNextSceneName; bool m_bShowStats; Leadwerks::Vec3 m_vGravity; uint8_t m_intPhysSteps; TransitionData m_hTransitionData; std::vector<StageActor*> m_vActors; void ClearScene(const bool bForce = true); bool LoadSceneFile(const std::string& pszFilePath, const bool bForce = true); void SetStageEvent(const StageEvent& hEvent, const bool bFireFunction = true); void PreTransition(); void PostTransition(); GameMenu* GetGameMenu(); public: Stage() {}; Stage(Leadwerks::Window* pWindow); virtual ~Stage(); // Time: void Pause(const bool bFireOutput = true); void Resume(const bool bFireOutput = true); const bool Paused(); uint64_t GetTime(); const bool TogglePause(const bool bHandleMouse = false); void Update(); void Clear(); bool SafeLoadSceneFile(const std::string& pszFilePath); StageEvent GetCurrentEvent(); Leadwerks::Entity* FindEntity(const std::string& pszName); StageActor* FindActor(const std::string& pszName); Leadwerks::Decal* CreateTempDecal(Leadwerks::Material* pMaterial); //std::string GetCurrentScene(); const bool InPlay(); void Reload(); void DrawStats(const bool bMode); void ToggleStats(); const bool ConsoleShowing(); void SetPhysSteps(const uint8_t iSteps); void SetVSync(const bool bState); Leadwerks::World* GetWorld(); Leadwerks::Camera* GetCamera(); Leadwerks::Context* GetFrameBuffer(); Leadwerks::Widget* GetGameMenuHUD(); void Transition(const std::string& pszFilePath, Leadwerks::Entity* pLandmark); void ShowHUD(); void HideHUD(); //Virtual functions: virtual void Start() {}; virtual void OnUpdate() {}; virtual void OnTick() {}; virtual void PostRender(Leadwerks::Context* pFrameBuffer) {}; virtual void OnNoPlay() {}; virtual void OnPostLoad(Leadwerks::Entity* pEntity) {}; virtual void OnIntermission() {}; virtual void OnTransition() {}; virtual void OnPlay() {}; virtual void OnProcessEvent(const Leadwerks::Event iEvent) {}; virtual void OnPaused() {}; virtual void OnResume() {}; virtual void ScheduleRestart() {}; static Stage* Create(Leadwerks::Window* pWindow); friend class StageActor; }; extern Stage* ActiveStage; extern Stage* CreateStage(Leadwerks::Window* pWindow); #endif // STAGE_H The first few months of development of this system still crashed randomly as I was hard loading the map instead of checking if a string was not empty every frame like how it's set up in the MyGame example. I went back to that approach and the crashing stopped. This did cause very hard to find bug which caused random restarts in then Release build. The issue ended up being that the string I was checking wasn't declared as empty. 
    With a great foundation by February 2021 , I went to getting the Cyclone stuff online. I mostly copied from the 2020 build but it was good enough to get started.  A quick model later, and I had my item dropper in game.
    https://cdn.discordapp.com/attachments/226834351982247936/815726095198322698/unknown.png
    The item dropper was the first item I had to rethink how physics objects would work. The original idea was to have the boxes be an actor that made the impact sounds and other effects when I got to it. However, the entity's actor doesn't get copied when it's instanced.  
    The solution was to have the item dropper create a the box model, and after it instances the model, it also assigns a collision hook to that model. I also apply modifications to the box friction and dampening.
    //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- uint64_t BoxImpactNoiseLastSoundTime; void PuzzleBoxCollisionHook(Entity* entity, Entity* entity2, const Vec3& position, const Vec3& normal, const float speed) { auto self = entity; auto target = entity2; if (self->GetWorld()->GetWaterMode()) { if (self->GetPosition().y < self->GetWorld()->GetWaterHeight()) { DMsg("Puzzlebox below water level. Deleting."); ReleaseGamePlayObject(self); return; } } float fixedSpeed = __FixSpeed(speed); float flSoftThreshhold = 1.5f; float flHardThreshhold = 4.0f; long intMaxFrequency = 300; if (fixedSpeed > flSoftThreshhold) { int collisiontype = target->GetCollisionType(); if (collisiontype == COLLISION_PROP || collisiontype == COLLISION_SCENE) { long t = Leadwerks::Time::GetCurrent(); if (t - BoxImpactNoiseLastSoundTime > intMaxFrequency) { BoxImpactNoiseLastSoundTime = t; if (fixedSpeed > flHardThreshhold) { // HARD self->EmitSound(GetImpactHardSound(), 20.0f, 0.5f, 1.0f, false); } else { // SOFT self->EmitSound(GetImpactSoftSound(), 20.0f, 0.5f, 1.0f, false); } } } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Leadwerks::Model* CreatePuzzleBox(const bool bAttactHook) { auto mdl = Model::Load(PATH_PUZZLEBOX_MDL); ApplyBoxSettings(mdl); if (bAttactHook) mdl->AddHook(Entity::CollisionHook, (void*)PuzzleBoxCollisionHook); return mdl; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Leadwerks::Model* SpawnPuzzleBox(Leadwerks::Model* ModelReference, const Vec3& vPosition) { if (ModelReference == NULL) { ModelReference = CreatePuzzleBox(); } auto newitem = (Model*)ModelReference->Instance(); newitem->SetPosition(vPosition); ApplyBoxSettings(newitem); newitem->AddHook(Entity::CollisionHook, (void*)PuzzleBoxCollisionHook); newitem->Show(); return newitem; } Although adding hooks to entities isn't really considered official, this is only one case out of many that I do this. It's really nice to write a quick function without creating and assigning an actor. The item dropper got improvements down the line to fix some physics issues. (I made the dropper very slippery so there was no friction and it stopped acting weird.) 
    The months rolled on and I kept chipping away at the project. My maps count was increasing every month but there something really bothering me. It was the fly movement.
    In Cyclone, I wanted a way for the player to launch themselves across rooms and deadly pits. I originally just applied velocity to the player character but the results felt terrible. I noticed that the wall launch was smoother if you held the button of the direction you were flying down. This gave me a nice arch instead of a sharp line. I ended up completely rewriting my player code from the ground up. Doing so allowed me to break apart what my Player actually was.
    First. there's the base Player class. This class alone will give you a spectator camera with input controls. Everything regarding the camera is defined in this base class. I also spent the time to work how HUD hints would work. The HUD uses the Leadwerks GUI system but for the HUD, I kept it to simple image drawing on the framebuffer. 
    The base class also controls ambient sound, although it's still a work in progress.
    Base Player Improvements - Work in Progress - Ultra Engine Community - Game Engine for VR
    //========= Copyright Reep Softworks, All rights reserved. ============// // // Purpose: // //=====================================================================// #ifndef PLAYERACTOR_H #define PLAYERACTOR_H #if defined( _WIN32 ) #pragma once #endif #include "pch.h" #include "Input.h" #include "../Classes/StageActor.h" enum CharacterMoveState { CHARACTERSTATE_IDLE = 0, CHARACTERSTATE_WALKING, CHARACTERSTATE_JUMPING, CHARACTERSTATE_FALLING, CHARACTERSTATE_FLYING }; enum ScreenEffect { SCREENEFFECT_NONE = 0, SCREENEFFECT_FADEIN, SCREENEFFECT_FADEOUT, SCREENEFFECT_FLASH, SCREENEFFECT_BLIND, }; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- class CommentaryData { public: std::string devname = ""; uint8_t index = 0; uint8_t maxindex = 0; Source* speaker = NULL; ~CommentaryData() { if (speaker) { speaker->Release(); speaker = NULL; } } }; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- class PlayerActor : public StageActor { PickInfo* m_pLookTrace; // HUD: bool m_bShowHint; bool m_bShowCommentary; bool m_bShowGameOver; uint64_t m_u64GameOverTime; CharacterMoveState m_hState; std::weak_ptr<CommentaryData> m_pCommentaryData; protected: Listener* m_pListener; bool m_bIsAlive; bool m_bAllowCrosshair; bool m_bSuspendMovement; bool m_bSuspendLook; bool m_bCrouched; bool m_bCrouching; bool m_bWantsToCrouch; Vec3 m_vPushForce; Vec3 m_vCamRotation; Vec3 m_vCameraOffset; float m_flEyeHeight; bool m_bFreeLook; bool m_bFreeMove; Pivot* m_pRotator; uint64_t m_u64QuickSpinInitTime; bool m_bSpinLockX; double m_dCamTopAngle; double m_dCamBtmAngle; float m_flEyeTraceDistance; float m_flInteractDistance; float m_flPickRadius; Action ACTION_MOVEFORWARD; Action ACTION_MOVEBACKWARD; Action ACTION_MOVERIGHT; Action ACTION_MOVELEFT; Action ACTION_INTERACTION; // Effects: ScreenEffect m_hEffectType; Vec4 m_vCurtianColor; float m_flCurtianRate; bool m_bZoomed; bool m_bZoomedIn; Vec3 m_vCamRotationOffset; Vec3 m_vSmoothedCamRotationOffset; const bool ActionHit(const Action actionname); const bool ActionDown(const Action actionname); void SetVelocity(const Vec3 vVelo); void AddForce(const Vec3 vForce); void ChangeMovementState(const CharacterMoveState hState); public: PlayerActor(); virtual ~PlayerActor(); virtual void Start(); virtual void UpdateWorld(); virtual void UpdateMatrix(); virtual bool IsPlayer() { return true; }; virtual bool IsAlive(); virtual void Kill(); virtual void Respawn() {}; void Kick(); virtual void Spawn() {}; virtual void UpdateKeyBindings(); // Quick functions for locking movement/look: virtual void SuspendMovement(const bool bValue) { m_bSuspendMovement = bValue; }; virtual void SuspendLook(const bool bValue) { m_bSuspendLook = bValue; }; // Camera functions: Camera* GetCamera(); Vec3 GetEyePosition(); Vec3 GetEyeAngles(); Quat GetEyeQAngles(); const float GetEyeHeight(); const float GetCrouchedEyeHeight(); virtual const bool CanUnCrouch() { return true; }; virtual void SetEyeHeight(const float flHeight); virtual void SetCameraAngles(Leadwerks::Vec3 vRot); virtual void Teleport(const Vec3& pNewPos, const Vec3& pNewRot, const Vec3& pNewVelocity); const bool FireUseOnEntity(Entity* pEntity); void SetFreeLookMode(const bool bMode); void SetFreeMoveMode(const bool bMode); void UpdateFreeLookMode(); void UpdateFreeMoveMode(); void QuickSpin(); virtual void UpdateCameraHeight(const bool bCrouchState); const float GetEyeTraceDistance() { return m_flEyeTraceDistance; }; Entity* GetEyeLookingAt(); const Vec3 GetEyeLookPositon(); PickInfo* GetEyeTrace() { return m_pLookTrace; }; const float GetPickRadius(); void ForceLookAtPoint(const Vec3& vPos, const bool bLockX = false); const Line3D GetLine(); // Movement + Physics virtual void SetPhysicsMode(const int iMode); const int GetPhysicsMode(); void SetWalkSpeed(const float iSpeed); const int GetWalkSpeed(); virtual void HandleMovement(); virtual const bool IsCrouched(); const bool IsAirbone(); virtual void HandleInteraction(); virtual void SetNoClipMode(const bool bState) {}; CharacterMoveState GetMovementState() { return m_hState; }; Vec3 GetVelocity(); virtual void Push(const int x, const int y, const int z); void ForceJump(const float fJump); // Interaction virtual void OnSuccessfullUse(Entity* pHitEntity) {}; virtual void OnUnSuccessfullUse() {}; virtual bool ShouldSkipUseTest() { return false; }; // Post drawing: void ShowCrosshair(const bool bShow); virtual void PostRender(Context* context); virtual void DrawHUD(Context* pContext); virtual void DrawCurtian(Context* pContext); Widget* GetHUD(); // Effects: virtual void HandleScreenEffects(); void SetCurtianColor(const Vec3& pColor); void PlayScreenEffect(const ScreenEffect iScreenEffect, const Vec3 vColor = Vec3(0), const float flRate = 0.1f); void PlayQuickFlash(const Vec3 vColor = Vec3(1)); void ClearScreenEffect(); void ZoomIn(const float fNewFov, const float flRate); void ZoomOut(const float flRate); // HUD Hint: void ShowHUDHint(const Action actionname, const std::string& pszMessage); void HideHUDHint(); void ShowCommentaryPanel(std::shared_ptr<CommentaryData> pData); void HideCommentaryPanel(); void ShowGameOverScreen(const std::string& pszMessage); }; extern const bool IsPlayerActor(Leadwerks::Entity* pEntity); extern PlayerActor* ToPlayerActor(Leadwerks::Entity* pEntity); extern PlayerActor* GetPlayerActor(); extern const bool IsPlayerLookingAtEntity(Leadwerks::Entity* pEntity); //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- class PlayerProxy : public StageActor { uint64_t m_u64TriggerTime; uint64_t m_u64DelayTime; protected: PlayerActor* GetPlayer(); const bool IsPlayerLookingAtMe(); public: PlayerProxy(); virtual void Start(); virtual void UpdatePhysics(); virtual void ReceiveSignal(const std::string& inputname, Entity* sender); virtual void Trigger() {}; }; class CommentaryNode : public PlayerProxy { bool m_bActive; std::shared_ptr<CommentaryData> m_hData; public: CommentaryNode(); virtual ~CommentaryNode(); virtual void Start(); virtual void UpdateWorld(); virtual bool Use(StageActor* pActor); virtual void Detach(); friend class CommentaryNode; }; class ProxyEndGame : public PlayerProxy { public: virtual void Trigger(); }; class ProxyCurtain : public PlayerProxy { ScreenEffect m_hType; uint64_t m_u64ShowTime; uint64_t m_u64TimeOut; public: // This needs to be PostStart as the player may be placed/loaded after this actor! virtual void PostStart(); virtual void UpdateWorld(); virtual void Trigger(); }; class ProxyHint : public PlayerProxy { uint64_t m_u64ShowTime; uint64_t m_u64TimeOut; public: virtual void Start(); virtual void UpdateWorld(); virtual void Trigger(); }; class ProxyAmbientSound : public PlayerProxy { Source* m_pSpeaker; float m_flMaxVolume; bool m_bActive; public: virtual void Start(); virtual void UpdatePhysics(); void Activate(); virtual void Detach(); friend class ProxyAmbientSound; }; class ProxyLookAtMe : public PlayerProxy { Model* m_pBox; public: virtual void Start(); Model* GetTargetBox(); }; #endif // PLAYERACTOR_H Next, I made an FPSPlayer which is built on top on the PlayerActor class. This transfroms the base class into a FPS character controller with interaction and physics pickup. It also has base code for a weapon. There is also a CyclonePlayer but it really is just a Start function that controls if it should give the player the weapon or not.
    //========= Copyright Reep Softworks, All rights reserved. ============// // // Purpose: // //=====================================================================// #ifndef FPSPLAYER_H #define FPSPLAYER_H #if defined( _WIN32 ) #pragma once #endif #include "pch.h" #include "PlayerActor.h" struct InteractPickupStoredData { float mass; int collisiontype; Vec2 dampinging; Vec2 friction; }; class FPSPlayer; class FPSWeapon : public StageActor { protected: Pivot* m_pSwayPivot; bool m_bHolstered; Model* m_pViewModel; Vec3 m_vModelOffset; Vec3 m_vBaseRotation; FPSPlayer* m_pOwner; public: FPSWeapon(); virtual void Holster() {}; virtual void Unholster() {}; virtual void UpdateHolster() {}; virtual void Fire(const int iMode = 0) {}; virtual void TestPickupState() {}; virtual void Reload() {}; virtual void BeginJump() {}; virtual void ReAdjustViewModel(const float flPlayerFOV); friend class FPSPlayer; }; class FPSWeaponPickup : public StageActor { void GiveWeapon(Entity* entity); public: virtual void Collision(Entity* entity, const Vec3& position, const Vec3& normal, float speed); virtual bool Use(StageActor* pActor); }; class FPSPlayer : public PlayerActor { // Movement + Physics bool m_bCrouchedOldState; bool m_bWalking; float m_flUpdateTick; uint64_t m_intLastStepTime; bool m_bLeftStep; std::string m_pszFootStepSounds[2]; Model* m_pCorpse; uint64_t m_u64PushLaunchTime; Vec3 m_flLastImpactSpeed; // Interaction Vec3 m_vCarryPosition; Quat m_vCarryQuat; Vec3 m_flThrowVelocity; float m_flPickDistance; float m_flCarryDistance; float m_flHoldThreshold; Pivot* m_pRotationCorrection; Joint* m_pEffector; Entity* m_pCarryEntity; protected: Action ACTION_JUMP; Action ACTION_CROUCH; Action ACTION_ZOOM; Action ACTION_FIREPRIMARY; Action ACTION_FIRESECONDARY; Action ACTION_RELOAD; // Weapon Pivot* m_pWeaponTag; //FPSWeapon* m_pActiveWeapon; virtual void PickupObject(Entity* pEntity); virtual void UpdateHeldObject(); virtual void ThrowObject(); void SetThrowVelocity(Vec3 vVelocity); public: FPSPlayer(); virtual ~FPSPlayer(); virtual void Spawn(); virtual void Kill(); virtual void Detach(); virtual void UpdateWorld(); virtual void UpdatePhysics(); virtual void Collision(Entity* entity, const Vec3& position, const Vec3& normal, float speed); virtual void UpdateKeyBindings(); virtual void Respawn(); // Movement + Physics virtual const bool IsCrouched(); virtual const bool CanUnCrouch(); virtual void HandleCrouching(); virtual void HandleCharacterController(); virtual void SetNoClipMode(const bool bState); virtual void UpdateMovement(Vec2 vMovement); virtual void OnUpdateMovement() {} virtual void OnPerStep(); virtual void OnStopMovement() {}; virtual void OnJump(); virtual void OnLand(); virtual bool GetCrouched() { return false; }; const bool IsFlying(); virtual void Push(const int x, const int y, const int z); const float GetFallSpeed(); // Interaction virtual void OnSuccessfullUse(Entity* pHitEntity); virtual void OnUnSuccessfullUse(); virtual bool ShouldSkipUseTest(); virtual void ForceDropObject(); const bool HoldingObject(); // Weapon void GiveWeapon(); FPSWeapon* GetWeapon(); virtual void BuildWeapon(FPSWeapon* pWeapon); void AdjustViewModel(); }; extern const bool IsFPSPlayer(Leadwerks::Entity* pEntity); extern FPSPlayer* ToFPSPlayer(Leadwerks::Entity* pEntity); #endif // FPSPLAYER_H The magic of getting the fly code to work correctly is in this bit. I also force the player to crouch in a ball to make it feel way less awkward. It was really tricky, but it all worked out.
    //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void FPSPlayer::UpdatePhysics() { HandleCharacterController(); UpdateHeldObject(); // This fixes a crash when dying. if (GetEntity()->GetPhysicsMode() == Entity::CharacterPhysics) { // Stop moving me if we're landed and it's been a bit since we were last pushed. if (m_u64PushLaunchTime > 0) { if (GetStage()->GetTime() > m_u64PushLaunchTime + 100) { if (m_vPushForce != Vec3(0) && !IsAirbone()) { DMsg("PlayerActor: Resetting push launch timer."); //GetEntity()->charactercontroller->stepheight = 0.51; m_vPushForce = Vec3(0); SetVelocity(m_vPushForce); m_bWantsToCrouch = false; m_u64PushLaunchTime = 0; } } } } if (GetMovementState() != CHARACTERSTATE_FALLING && GetEntity()->GetVelocity().y < -4.5f) { ChangeMovementState(CHARACTERSTATE_FALLING); } else if (GetMovementState() == CHARACTERSTATE_FALLING && GetEntity()->GetVelocity().y >= 0.0f) { ChangeMovementState(CHARACTERSTATE_IDLE); OnLand(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void FPSPlayer::HandleCharacterController() { if (GetStage()->ConsoleShowing() || m_bSuspendMovement) return; if (GetEntity()->GetPhysicsMode() == Entity::RigidBodyPhysics) return; float jumpinput = m_vPushForce.y; Vec2 movement = Input::GetActionAxis(Input::ToActionAxis(ACTION_MOVEFORWARD, ACTION_MOVEBACKWARD, ACTION_MOVELEFT, ACTION_MOVERIGHT, GAMEPADAXIS_LSTICK)); float speed = g_player_walkspeed; // Handle Crouching HandleCrouching(); if (IsCrouched()) speed = g_player_walkspeed / 2; auto l = movement.Length(); if (l > 0.0) { movement.x = (movement.x / l) * speed; movement.y = (movement.y / l) * speed; } // Handle Jumping if (ActionHit(ACTION_JUMP)) { if (!IsAirbone()) { jumpinput = g_player_jumpforce; ChangeMovementState(CHARACTERSTATE_JUMPING); OnJump(); if (movement.y != 0) movement.y = movement.y * 1.6; if (movement.x == 0 || movement.y == 0) OnPerStep(); } } if (m_u64PushLaunchTime > 0) { if (m_vPushForce.x != 0 || m_vPushForce.z != 0) { movement = Vec2(0); movement += GetEntity()->GetVelocity(true).xz(); //DMsg(movement.ToString()); // Hackory to make the player crouch properly while flying. m_bWantsToCrouch = false; m_bCrouched = true; m_vCameraOffset.y = GetCrouchedEyeHeight(); GetEntity()->SetInput(0, movement.y, movement.x, m_vPushForce.y, m_bCrouched, g_player_maxdecel, g_player_mindecel, true); } } else { UpdateMovement(movement); GetEntity()->SetInput(GetEyeAngles().y, movement.y, movement.x, jumpinput, m_bCrouched, g_player_maxdecel, g_player_mindecel, true); } m_vPushForce.y = 0; } Along with some adjustments to the Cyclone code, Everything was working much better. I continued to make tweaks and changes as I continued along.  A nice bonus was this fixed Cyclones on Ramps which didn't work with my old code which helped me to get another map done.
    Revamped Cyclone Behavior - Work in Progress - Ultra Engine Community - Game Engine for VR
    After 6 months of development, I'm very happy to say that today I hit my first milestone. I have 10 playable maps with everything feeling acceptable. Most gameplay functions are online AND today I squeezed an Addon System to allow custom maps. The game runs with the Lua Sandbox enabled so custom maps can code new content in Lua!
     
    However, the entire game looks like this.

    I think it's safe to say that it's time to close Visual Studio for a bit and open Blender and Gimp on a daily basis. I'll have to open Visual Studio again to code the indicator strips and other effects but I'm really excited to just zone out and work models and textures. (although I don't see myself as the best in that area.) If you're actually interested in helping, my DM is open.
    Right now the plan is to get some maps art up for some screenshots and by late September/October set up Cyclone for Steamworks for an Early Access release Q1 2022. I can beat the game within a few minutes but with the project having zero story it can get more content in the future by me or others thanks to the addon system.
    First thing I need is a logo....
  2. reepblue

    Code
    Cyclone was shaping up visually, but I was getting feedback about the sound implementation not being so good. I previously created a script system that defined all my sounds under profiles. This was a step in the right direction and made it easier to tweak sounds without recompiling code. However, no matter how much I played with variables, I never was able to satisfy the complaints. 
    After I showcased my first gameplay trailer, I decided to finally sit down and really understand FMOD. I was really overwhelmed at first as I was looking at the core api, but then I was suggested to use the FMOD Studio API instead. 
    FMOD studio is a nifty program that allows much more fine control how sounds are played in a 3D environment. You can also add parameters to alter the sounds with code in real time. In this example, the engine sound can be changed via the RPM and Load values given. (You can check the examples of the SDK on how to do this.)

    FMOD Studio packs everything into bank files. You can have as many banks as you want, but for my game, I only use one. 
    To load a bank file, I just do a for loop in a directory called Media.
    void Initialize() { if (!initialized) { Msg("Initializing FMOD sound driver..."); FMOD::Studio::System::create(&system); system->getCoreSystem(&coreSystem); coreSystem->setSoftwareFormat(0, FMOD_SPEAKERMODE_DEFAULT, 0); system->initialize(1024, FMOD_STUDIO_INIT_NORMAL, FMOD_INIT_NORMAL, NULL); system->setNumListeners(1); system->update(); //Load any .bank files in main directory Leadwerks::Directory* dir = Leadwerks::FileSystem::LoadDir("Media/"); if (dir) { for (std::size_t i = 0; i < dir->files.size(); i++) { std::string file = dir->files[i]; std::string ext = Leadwerks::String::Lower(Leadwerks::FileSystem::ExtractExt(file)); if (ext == "bank") { std::string fullpath = Leadwerks::FileSystem::RealPath("Media/" + file); Msg("Loading FMOD bank file \"" + fullpath + "\"..."); FMOD::Studio::Bank* bnk = NULL; FMOD_RESULT err = system->loadBankFile(fullpath.c_str(), FMOD_STUDIO_LOAD_BANK_NORMAL, &bnk); if (err == FMOD_ERR_FILE_NOTFOUND || err == FMOD_ERR_FILE_BAD) Msg("Error: Failed to load FMOD bank file \"" + fullpath + "\"..."); } } delete dir; } initialized = true; } } Another thing to note is that you manually need to update FMOD each frame. I've created my own wrapper update function and call it in my Stage class. This also calls my Initialize function.
    void Update() { FMODEngine::Initialize(); if (!initialized) Initialize(); if (system != NULL) system->update(); } Now, that we have the basics in, we first need a listener which was actually the hardest part. At first, I wasn't sure how I can match the location and rotation calculations from Leadwerks to FMOD. I first tried implementing the position and thankfully, FMOD uses the Left-Handed system by default. 
    Rotation was a bit difficult for me but grabbing random code from stackoverflow saved me again and pushing the Y rotation 90 degrees made the 1:1 match. 
    void UpdateListenerPosition(Leadwerks::Entity* pOwner) { if (pOwner == NULL || system == NULL) return; Leadwerks::Vec3 entity_position = pOwner->GetPosition(true); // Position the listener at the origin FMOD_3D_ATTRIBUTES attributes = { { 0 } }; attributes.position.x = entity_position.x; attributes.position.y = entity_position.y; attributes.position.z = entity_position.z; float radians = Leadwerks::Math::DegToRad(pOwner->GetRotation(true).y + 90); float fx = cos(radians); float fz = sin(radians); attributes.forward.x = -fx; attributes.forward.z = fz; attributes.up.y = 1.0f; system->setListenerAttributes(0, &attributes); } There's nothing to create since a listener is initialized with the system. You just need to update its attributes each frame. I call the above function in my UpdateMatrix hook for my main camera. 
    Last, we need a way to play simple sounds. 2D and 3D sounds are defined in the bank file, but to play a sound with no 3D settings, it's as simple as the following.
    void EmitSound(const std::string& pszSoundEvent) { FMOD::Studio::EventDescription* pDest; std::string fulldsp = "event:/" + pszSoundEvent; system->getEvent(fulldsp.c_str(), &pDest); FMOD::Studio::EventInstance* instance; pDest->createInstance(&instance); instance->start(); instance->release(); } And to play a sound at an entity's location, this has been working for me so far.
    void EmitSoundFromEntity(const std::string& pszSoundEvent, ENTITY_CLASS pEntity) { FMOD::Studio::EventDescription* pDest; std::string fulldsp = "event:/" + pszSoundEvent; system->getEvent(fulldsp.c_str(), &pDest); FMOD::Studio::EventInstance* instance; pDest->createInstance(&instance); FMOD_3D_ATTRIBUTES attributes = { { 0 } }; attributes.forward.z = 1.0f; attributes.up.y = 1.0f; attributes.position.x = pEntity->GetPosition(true).x; attributes.position.y = pEntity->GetPosition(true).y; attributes.position.z = pEntity->GetPosition(true).z; instance->set3DAttributes(&attributes); instance->start(); instance->release(); } All you need to pass is the name of the event. If you decided to use subfolders, you'll need to include them in the parameter. It acts like a virtual directory.
    For anything with more control (such as loops, playback, etc) we're going to need our own Source/Speaker class.  This class just stores the event instance and acts like the standard Source/Speaker class.  I also support both the engine's OpenAL implementation and FMOD via a switch, but my actors only care about this game speaker class and the EmitSound functions. 
    class GameSpeaker { #ifdef USE_FMOD FMOD::Studio::EventDescription* m_pDest; FMOD::Studio::EventInstance* m_pEventInstance; int m_iLength; #endif // USE_FMOD Leadwerks::Source* m_pOpenALSpeaker; protected: Leadwerks::Vec3 m_vPosition; int m_iState; public: GameSpeaker(); ~GameSpeaker(); void Play(); void Stop(); void Pause(); void Resume(); void Reset(); void SetVolume(const float flVolume); void SetPitch(const float flPitch); void SetPosition(const Leadwerks::Vec3& vPosition); void SetTime(const float flTime); const float GetVolume(); const float GetPitch(); const float GetTime(); const int GetState(); const float GetLength(); const Leadwerks::Vec3& GetPosition(); void BuildSpeaker(const std::string& pszEventName); static GameSpeaker* Create(const std::string& pszEventName); static const int Stopped; static const int Playing; static const int Paused; }; extern std::shared_ptr<GameSpeaker> CreateGameSpeaker(const std::string& pszEventName); I still have very minor things to work out, but overall, this has been a success. This also could be implemented exactly the same way in Ultra Engine if you're reading this in the future. 
    Basic FMOD Implementation in Cyclone - Work in Progress - Ultra Engine Community - Game Engine for VR
  3. reepblue
    With the new engine coming along and me noticing limits with Cyclone's current architecture, it's time for me to start thinking about the mistakes I've made and how would avoid repeating them with the new engine. Luckly, Ultra Engine is far more flexible than Leadwerks thanks to smart pointers and the event system. I wish to share some of my plans going forward.
    Log, Log, Log Everything!
    I personally never look at my log files. I instead look at the CMD output and the debugger to see what's going on. However, end users don't have that luxury. The engine will automatically log its warnings and error messages, but it'll not tell you when that message was printed or the events that led up to it. Creating your own log stream like this will give you far more control over your log file.
    // Prepare the log file. auto logfile = WriteFile("Game.log"); // Main loop bool running = true; while (running) { while (PeekEvent()) { const auto e = WaitEvent(); if (e.id == EVENT_PRINT) logfile->WriteLine(e.text.ToString()); } } logfile->Close(); logfile = NULL; You can easily do add time stamp before the message, so you have an idea when the event actually happened. You can use real world clock time or the current application time.
    Do Nothing OS Specific (Unless It's Aesthetic)
    Cyclone is a Win32 application. Period. It works wonderfully on Proton and there isn't any reason for me to install an outdated version of Ubuntu and waste time making a native build for Linux that'll only sometimes work. I took the liberty of using the Windows API to create my own Input System, XInput integration, and the Splash Window. I also created a custom DLL for my Input System so the Action Mapper application would use the same system as the game. 
    Going forward, I want my code to work wherever Ultra Engine can run on. This means sticking to the API for mostly everything unless I need to use a 3rd party library (Like Steamworks). Still, 98% of your users will be using Windows so you might as well add some nice touches like the application knowing what theme the user is using and dressing your application accordingly like this.
    Launch Arguments != Settings
    If you've programmed with Leadwerks, you should be familiar with System::SetProperty() and System::GetProperty(). This by default will register a launch argument as a setting and then save it to a settings file. Cyclone works like this too, but arguments don't get saved to the settings file to avoid confusion. 
    I never used arguments to override settings. I only used -devmode, -console, -fullscreen, +screenwidth and +screenheight. So, instead of mixing launch arguments with user settings, I think just going to keep them separated and only use the arguments to tell the application how to launch. 
    Everything Will Be One
    Cyclone ships with 2 applications. One being the main game, and the other being the Action Mapper for key binding. They both share a dynamic library. This is because Cyclone is made using Leadwerks and the Action Mapper is made using Ultra App Kit. I want to make a Console window to remove it from the UI and a dedicated window for graphic settings would be handy. My new approach will make these separate windows that can be called with a launch command. 
    Compartmentalize Everything
    I ran into many conflicts when adding working on the Flag Run update. Cyclone was structured to be a Puzzle game, and here I was adding clocks and an auto reload system. I had to do a lot of edits to my code to make it work and not break the older maps. 
    Because of how Leadwerks worked, I had to do a lot of mixing and cross referencing of classes. In the end, it's all very messy now and it discourages me from adding to it more. My game application code will only create the window, framebuffer and camera. It's also will be responsible handle scenes and emit events. The components should do everything else. I plan to make a component for the always existing camera entity and have it listen to events when to show UI elements or change its settings. I can make the UI and settings components separate too! 
    Since there will be multiple "apps" in one application, I kind of have to do this. Thankfully, I don't have to do things like create my own reflection system this time...
    Get The Essentials Right, The First Time
    When you're making your first game, you generally want it up and running as soon as possible. Everything else critical you'll say "Meh, I'll worry about that later". Why would you spend time on things like an Input System or A Sound System if you don't even know the game is fun? A month or so later, you find yourself figuring out how the new systems you just built should be integrated with the existing game code. It's not funny, it's not fun. 
    I already have a game, and I know what works so I don't need to repeat that again. And being that the engine isn't 100% yet, I have plenty of time to get the essentials right. Here is what I'll be spending the first few months on.
    Ensuring that windows display and activate correctly. A splash window should show up, and the next window should be brought forward 100% of the time. Cyclone has a bug right now which a game window doesn't get brought to the top although I'm using every API call to make it do so. Settings system should be in-place before drawing a 3D cube to a framebuffer.  I have a very good input system in Cyclone already, but it relies heavily on WinAPI to work. I also include XInput code which gets overwritten by Steam Input anyway. The input system will now need to use the engine's API instead of Windows.  This system will work exactly the same as I have it today, but I think I'm going to leave out controller integration for now. I really liked using FMOD, but to lower dependencies, I think I should try the new OpenAL-Soft sound engine. I plan for my new system to read json scripts for sound entries that contain data for the Speaker and close captioning to display on the screen.    I plan to get cracking within a few weeks! I set up my PC with the RTX 3600 running Windows 10 to develop and stream to Discord! I noticed a lot of slow down with my GTX 1050 running the engine in fullscreen so it's time to use something with more power. I am keeping an eye out for a good price on a 4070 Ti though.  
×
×
  • Create New...