diff --git a/Documentation/ScriptingAPI-Documentation.md b/Documentation/ScriptingAPI-Documentation.md index 0e540452a..13849c873 100644 --- a/Documentation/ScriptingAPI-Documentation.md +++ b/Documentation/ScriptingAPI-Documentation.md @@ -358,6 +358,9 @@ Describes an orientation in 3D space. - Play() - Stop() - Pause() +- SetLooped(bool value) +- IsLooped() : bool result +- IsPlaying() : bool result #### MaterialComponent - SetBaseColor() @@ -384,7 +387,9 @@ This is the main entry point and manages the lifetime of the application. Even t - GetContent() : Resource? result - GetActivePath() : RenderPath? result - SetActivePath(RenderPath path, opt float fadeSeconds,fadeColorR,fadeColorG,fadeColorB) -- SetFrameSkip(bool enabled) +- SetFrameSkip(bool enabled) -- enable/disable frame skipping in fixed update +- SetTargetFrameRate(float fps) -- set target frame rate for fixed update and variable rate update when frame rate is locked +- SetFrameRateLock(bool enabled) -- if enabled, variable rate update will use a fixed delta time - SetInfoDisplay(bool active) - SetWatermarkDisplay(bool active) - SetFPSDisplay(bool active) diff --git a/Editor/ForceFieldWindow.cpp b/Editor/ForceFieldWindow.cpp index f52973fa9..73cc8072a 100644 --- a/Editor/ForceFieldWindow.cpp +++ b/Editor/ForceFieldWindow.cpp @@ -72,7 +72,7 @@ ForceFieldWindow::ForceFieldWindow(wiGUI* gui) : GUI(gui) ForceFieldComponent* force = wiSceneSystem::GetScene().forces.GetComponent(entity); if (force != nullptr) { - force->range = args.fValue; + force->range_local = args.fValue; } }); rangeSlider->SetEnabled(false); @@ -119,7 +119,7 @@ void ForceFieldWindow::SetEntity(Entity entity) { typeComboBox->SetSelected(force->type == ENTITY_TYPE_FORCEFIELD_POINT ? 0 : 1); gravitySlider->SetValue(force->gravity); - rangeSlider->SetValue(force->range); + rangeSlider->SetValue(force->range_local); forceFieldWindow->SetEnabled(true); } diff --git a/Editor/LightWindow.cpp b/Editor/LightWindow.cpp index 4946dd044..97ae5471f 100644 --- a/Editor/LightWindow.cpp +++ b/Editor/LightWindow.cpp @@ -42,7 +42,7 @@ LightWindow::LightWindow(wiGUI* gui) : GUI(gui) LightComponent* light = wiSceneSystem::GetScene().lights.GetComponent(entity); if (light != nullptr) { - light->range = args.fValue; + light->range_local = args.fValue; } }); rangeSlider->SetEnabled(false); @@ -246,7 +246,7 @@ void LightWindow::SetEntity(Entity entity) //lightWindow->SetEnabled(true); energySlider->SetEnabled(true); energySlider->SetValue(light->energy); - rangeSlider->SetValue(light->range); + rangeSlider->SetValue(light->range_local); radiusSlider->SetValue(light->radius); widthSlider->SetValue(light->width); heightSlider->SetValue(light->height); diff --git a/WickedEngine/MainComponent.cpp b/WickedEngine/MainComponent.cpp index 015e1a415..dffad1157 100644 --- a/WickedEngine/MainComponent.cpp +++ b/WickedEngine/MainComponent.cpp @@ -123,7 +123,14 @@ void MainComponent::Run() wiProfiler::BeginFrame(); wiProfiler::BeginRange("CPU Frame", wiProfiler::DOMAIN_CPU); - deltaTime = float(max(0, timer.elapsed() / 1000.0)); + if (framerate_lock) + { + deltaTime = 1.0f / targetFrameRate; + } + else + { + deltaTime = float(max(0, timer.elapsed() / 1000.0)); + } timer.record(); if (wiWindowRegistration::IsWindowActive()) diff --git a/WickedEngine/MainComponent.h b/WickedEngine/MainComponent.h index f18093f79..d1aae436e 100644 --- a/WickedEngine/MainComponent.h +++ b/WickedEngine/MainComponent.h @@ -13,6 +13,7 @@ protected: RenderPath* activePath = nullptr; float targetFrameRate = 60; bool frameskip = true; + bool framerate_lock = false; bool initialized = false; wiFadeManager fadeManager; @@ -44,6 +45,7 @@ public: // enabled : the FixedUpdate() loop will run at targetFrameRate frequency // disabled : the FixedUpdate() loop will run every frame only once. void setFrameSkip(bool enabled) { frameskip = enabled; } + void setFrameRateLock(bool enabled) { framerate_lock = enabled; } // This is where the critical initializations happen (before any rendering or anything else) virtual void Initialize(); diff --git a/WickedEngine/MainComponent_BindLua.cpp b/WickedEngine/MainComponent_BindLua.cpp index 01ed42de9..ac94f874a 100644 --- a/WickedEngine/MainComponent_BindLua.cpp +++ b/WickedEngine/MainComponent_BindLua.cpp @@ -17,6 +17,8 @@ Luna::FunctionType MainComponent_BindLua::methods[] = { lunamethod(MainComponent_BindLua, GetActivePath), lunamethod(MainComponent_BindLua, SetActivePath), lunamethod(MainComponent_BindLua, SetFrameSkip), + lunamethod(MainComponent_BindLua, SetTargetFrameRate), + lunamethod(MainComponent_BindLua, SetFrameRateLock), lunamethod(MainComponent_BindLua, SetInfoDisplay), lunamethod(MainComponent_BindLua, SetWatermarkDisplay), lunamethod(MainComponent_BindLua, SetFPSDisplay), @@ -235,6 +237,40 @@ int MainComponent_BindLua::SetFrameSkip(lua_State *L) wiLua::SError(L, "SetFrameSkip(bool enabled) not enought arguments!"); return 0; } +int MainComponent_BindLua::SetTargetFrameRate(lua_State *L) +{ + if (component == nullptr) + { + wiLua::SError(L, "SetTargetFrameRate(float value) component is empty!"); + return 0; + } + + int argc = wiLua::SGetArgCount(L); + if (argc > 0) + { + component->setTargetFrameRate(wiLua::SGetFloat(L, 1)); + } + else + wiLua::SError(L, "SetTargetFrameRate(float value) not enought arguments!"); + return 0; +} +int MainComponent_BindLua::SetFrameRateLock(lua_State *L) +{ + if (component == nullptr) + { + wiLua::SError(L, "SetFrameRateLock(bool enabled) component is empty!"); + return 0; + } + + int argc = wiLua::SGetArgCount(L); + if (argc > 0) + { + component->setFrameRateLock(wiLua::SGetBool(L, 1)); + } + else + wiLua::SError(L, "SetFrameRateLock(bool enabled) not enought arguments!"); + return 0; +} int MainComponent_BindLua::SetInfoDisplay(lua_State *L) { if (component == nullptr) diff --git a/WickedEngine/MainComponent_BindLua.h b/WickedEngine/MainComponent_BindLua.h index ed239054e..fc8ff8e6f 100644 --- a/WickedEngine/MainComponent_BindLua.h +++ b/WickedEngine/MainComponent_BindLua.h @@ -20,6 +20,8 @@ public: int GetActivePath(lua_State *L); int SetActivePath(lua_State *L); int SetFrameSkip(lua_State *L); + int SetTargetFrameRate(lua_State *L); + int SetFrameRateLock(lua_State *L); int SetInfoDisplay(lua_State *L); int SetWatermarkDisplay(lua_State *L); int SetFPSDisplay(lua_State *L); diff --git a/WickedEngine/WickedEngine_SHARED.vcxitems b/WickedEngine/WickedEngine_SHARED.vcxitems index bb8e3005b..1dd72da03 100644 --- a/WickedEngine/WickedEngine_SHARED.vcxitems +++ b/WickedEngine/WickedEngine_SHARED.vcxitems @@ -727,6 +727,7 @@ + diff --git a/WickedEngine/WickedEngine_SHARED.vcxitems.filters b/WickedEngine/WickedEngine_SHARED.vcxitems.filters index 67acb47a0..58ea76aec 100644 --- a/WickedEngine/WickedEngine_SHARED.vcxitems.filters +++ b/WickedEngine/WickedEngine_SHARED.vcxitems.filters @@ -1978,6 +1978,9 @@ scripts + + scripts + diff --git a/WickedEngine/wiRenderer.cpp b/WickedEngine/wiRenderer.cpp index fc4b1de73..ba468553a 100644 --- a/WickedEngine/wiRenderer.cpp +++ b/WickedEngine/wiRenderer.cpp @@ -1235,7 +1235,7 @@ struct SHCAM void CreateSpotLightShadowCam(const LightComponent& light, SHCAM& shcam) { const float zNearP = 0.1f; - const float zFarP = max(1.0f, light.range); + const float zFarP = max(1.0f, light.GetRange()); shcam = SHCAM(XMFLOAT4(0, 0, 0, 1), zNearP, zFarP, light.fov); shcam.Update(XMMatrixRotationQuaternion(XMLoadFloat4(&light.rotation)) * XMMatrixTranslationFromVector(XMLoadFloat3(&light.position))); @@ -3967,7 +3967,7 @@ void UpdateRenderData(GRAPHICSTHREAD threadID) entityArray[entityCounter].SetType(light.GetType()); entityArray[entityCounter].positionWS = light.position; XMStoreFloat3(&entityArray[entityCounter].positionVS, XMVector3TransformCoord(XMLoadFloat3(&entityArray[entityCounter].positionWS), viewMatrix)); - entityArray[entityCounter].range = light.range; + entityArray[entityCounter].range = light.GetRange(); entityArray[entityCounter].color = wiMath::CompressColor(light.color); entityArray[entityCounter].energy = light.energy; entityArray[entityCounter].shadowBias = light.shadowBias; @@ -4055,8 +4055,8 @@ void UpdateRenderData(GRAPHICSTHREAD threadID) entityArray[entityCounter].SetType(force.type); entityArray[entityCounter].positionWS = force.position; entityArray[entityCounter].energy = force.gravity; - entityArray[entityCounter].range = 1.0f / max(0.0001f, force.range); // avoid division in shader - entityArray[entityCounter].coneAngleCos = force.range; // this will be the real range in the less common shaders... + entityArray[entityCounter].range = 1.0f / max(0.0001f, force.GetRange()); // avoid division in shader + entityArray[entityCounter].coneAngleCos = force.GetRange(); // this will be the real range in the less common shaders... // The default planar force field is facing upwards, and thus the pull direction is downwards: entityArray[entityCounter].directionWS = force.direction; @@ -4531,7 +4531,7 @@ void DrawLights(const CameraComponent& camera, GRAPHICSTHREAD threadID) { MiscCB miscCb; miscCb.g_xColor.x = float(entityArrayOffset_Lights + i); - float sca = light.range + 1; + float sca = light.GetRange() + 1; XMStoreFloat4x4(&miscCb.g_xTransform, XMMatrixTranspose(XMMatrixScaling(sca, sca, sca)*XMMatrixTranslationFromVector(XMLoadFloat3(&light.position)) * camera.GetViewProjection())); device->UpdateBuffer(&constantBuffers[CBTYPE_MISC], &miscCb, threadID); device->BindConstantBuffer(VS, &constantBuffers[CBTYPE_MISC], CB_GETBINDSLOT(MiscCB), threadID); @@ -4546,7 +4546,7 @@ void DrawLights(const CameraComponent& camera, GRAPHICSTHREAD threadID) miscCb.g_xColor.x = float(entityArrayOffset_Lights + i); const float coneS = (const float)(light.fov / XM_PIDIV4); XMStoreFloat4x4(&miscCb.g_xTransform, XMMatrixTranspose( - XMMatrixScaling(coneS*light.range, light.range, coneS*light.range)* + XMMatrixScaling(coneS*light.GetRange(), light.GetRange(), coneS*light.GetRange())* XMMatrixRotationQuaternion(XMLoadFloat4(&light.rotation))* XMMatrixTranslationFromVector(XMLoadFloat3(&light.position)) * camera.GetViewProjection() @@ -4597,11 +4597,11 @@ void DrawLightVisualizers(const CameraComponent& camera, GRAPHICSTHREAD threadID VolumeLightCB lcb; lcb.lightColor = XMFLOAT4(light.color.x, light.color.y, light.color.z, 1); - lcb.lightEnerdis = XMFLOAT4(light.energy, light.range, light.fov, light.energy); + lcb.lightEnerdis = XMFLOAT4(light.energy, light.GetRange(), light.fov, light.energy); if (type == LightComponent::POINT) { - lcb.lightEnerdis.w = light.range*light.energy*0.01f; // scale + lcb.lightEnerdis.w = light.GetRange()*light.energy*0.01f; // scale XMStoreFloat4x4(&lcb.lightWorld, XMMatrixTranspose( XMMatrixScaling(lcb.lightEnerdis.w, lcb.lightEnerdis.w, lcb.lightEnerdis.w)* camrot* @@ -4615,7 +4615,7 @@ void DrawLightVisualizers(const CameraComponent& camera, GRAPHICSTHREAD threadID else if (type == LightComponent::SPOT) { float coneS = (float)(light.fov / 0.7853981852531433); - lcb.lightEnerdis.w = light.range*light.energy*0.03f; // scale + lcb.lightEnerdis.w = light.GetRange()*light.energy*0.03f; // scale XMStoreFloat4x4(&lcb.lightWorld, XMMatrixTranspose( XMMatrixScaling(coneS*lcb.lightEnerdis.w, lcb.lightEnerdis.w, coneS*lcb.lightEnerdis.w)* XMMatrixRotationQuaternion(XMLoadFloat4(&light.rotation))* @@ -4737,7 +4737,7 @@ void DrawVolumeLights(const CameraComponent& camera, GRAPHICSTHREAD threadID) { MiscCB miscCb; miscCb.g_xColor.x = float(entityArrayOffset_Lights + i); - float sca = light.range + 1; + float sca = light.GetRange() + 1; XMStoreFloat4x4(&miscCb.g_xTransform, XMMatrixTranspose(XMMatrixScaling(sca, sca, sca)*XMMatrixTranslationFromVector(XMLoadFloat3(&light.position)) * camera.GetViewProjection())); device->UpdateBuffer(&constantBuffers[CBTYPE_MISC], &miscCb, threadID); device->BindConstantBuffer(VS, &constantBuffers[CBTYPE_MISC], CB_GETBINDSLOT(MiscCB), threadID); @@ -4752,7 +4752,7 @@ void DrawVolumeLights(const CameraComponent& camera, GRAPHICSTHREAD threadID) miscCb.g_xColor.x = float(entityArrayOffset_Lights + i); const float coneS = (const float)(light.fov / XM_PIDIV4); XMStoreFloat4x4(&miscCb.g_xTransform, XMMatrixTranspose( - XMMatrixScaling(coneS*light.range, light.range, coneS*light.range)* + XMMatrixScaling(coneS*light.GetRange(), light.GetRange(), coneS*light.GetRange())* XMMatrixRotationQuaternion(XMLoadFloat4(&light.rotation))* XMMatrixTranslationFromVector(XMLoadFloat3(&light.position)) * camera.GetViewProjection() @@ -5059,7 +5059,7 @@ void DrawForShadowMap(const CameraComponent& camera, GRAPHICSTHREAD threadID, ui SHCAM shcam; CreateSpotLightShadowCam(light, shcam); - const float zFarP = max(1.0f, light.range); + const float zFarP = max(1.0f, light.GetRange()); Frustum frustum; frustum.Create(shcam.realProjection, shcam.View, zFarP); @@ -5137,7 +5137,7 @@ void DrawForShadowMap(const CameraComponent& camera, GRAPHICSTHREAD threadID, ui for (size_t i = 0; i < scene.aabb_objects.GetCount(); ++i) { const AABB& aabb = scene.aabb_objects[i]; - if (SPHERE(light.position, light.range).intersects(aabb)) + if (SPHERE(light.position, light.GetRange()).intersects(aabb)) { const ObjectComponent& object = scene.objects[i]; if (object.IsRenderable() && object.IsCastingShadow() && object.GetRenderTypes() == RENDERTYPE_OPAQUE) @@ -5171,7 +5171,7 @@ void DrawForShadowMap(const CameraComponent& camera, GRAPHICSTHREAD threadID, ui device->BindConstantBuffer(PS, &constantBuffers[CBTYPE_MISC], CB_GETBINDSLOT(MiscCB), threadID); const float zNearP = 0.1f; - const float zFarP = max(1.0f, light.range); + const float zFarP = max(1.0f, light.GetRange()); SHCAM cameras[] = { SHCAM(XMFLOAT4(0.5f, -0.5f, -0.5f, -0.5f), zNearP, zFarP, XM_PIDIV2), //+x SHCAM(XMFLOAT4(0.5f, 0.5f, 0.5f, -0.5f), zNearP, zFarP, XM_PIDIV2), //-x diff --git a/WickedEngine/wiSceneSystem.cpp b/WickedEngine/wiSceneSystem.cpp index c604d823d..fe1f88332 100644 --- a/WickedEngine/wiSceneSystem.cpp +++ b/WickedEngine/wiSceneSystem.cpp @@ -1245,7 +1245,7 @@ namespace wiSceneSystem LightComponent& light = lights.Create(entity); light.energy = energy; - light.range = range; + light.range_local = range; light.fov = XM_PIDIV4; light.color = color; light.SetType(LightComponent::POINT); @@ -1269,7 +1269,7 @@ namespace wiSceneSystem ForceFieldComponent& force = forces.Create(entity); force.gravity = 0; - force.range = 0; + force.range_local = 0; force.type = ENTITY_TYPE_FORCEFIELD_POINT; return entity; @@ -1972,7 +1972,7 @@ namespace wiSceneSystem XMStoreFloat3(&force.position, T); XMStoreFloat3(&force.direction, XMVector3Normalize(XMVector3TransformNormal(XMVectorSet(0, -1, 0, 0), W))); - force.range = force.range_local * max(XMVectorGetX(S), max(XMVectorGetY(S), XMVectorGetZ(S))); + force.range_global = force.range_local * max(XMVectorGetX(S), max(XMVectorGetY(S), XMVectorGetZ(S))); }); } void RunLightUpdateSystem( @@ -1998,7 +1998,7 @@ namespace wiSceneSystem XMStoreFloat4(&light.rotation, R); XMStoreFloat3(&light.direction, XMVector3TransformNormal(XMVectorSet(0, 1, 0, 0), W)); - light.range = light.range_local * max(XMVectorGetX(S), max(XMVectorGetY(S), XMVectorGetZ(S))); + light.range_global = light.range_local * max(XMVectorGetX(S), max(XMVectorGetY(S), XMVectorGetZ(S))); switch (light.type) { @@ -2006,10 +2006,10 @@ namespace wiSceneSystem aabb.createFromHalfWidth(wiRenderer::GetCamera().Eye, XMFLOAT3(10000, 10000, 10000)); break; case LightComponent::SPOT: - aabb.createFromHalfWidth(light.position, XMFLOAT3(light.range, light.range, light.range)); + aabb.createFromHalfWidth(light.position, XMFLOAT3(light.GetRange(), light.GetRange(), light.GetRange())); break; case LightComponent::POINT: - aabb.createFromHalfWidth(light.position, XMFLOAT3(light.range, light.range, light.range)); + aabb.createFromHalfWidth(light.position, XMFLOAT3(light.GetRange(), light.GetRange(), light.GetRange())); break; case LightComponent::SPHERE: case LightComponent::DISC: diff --git a/WickedEngine/wiSceneSystem.h b/WickedEngine/wiSceneSystem.h index c31790dcb..9c72c239c 100644 --- a/WickedEngine/wiSceneSystem.h +++ b/WickedEngine/wiSceneSystem.h @@ -662,7 +662,7 @@ namespace wiSceneSystem // Non-serialized attributes: XMFLOAT3 position; - float range; + float range_global; XMFLOAT3 direction; XMFLOAT4 rotation; XMFLOAT3 front; @@ -681,7 +681,7 @@ namespace wiSceneSystem inline bool IsVisualizerEnabled() const { return _flags & VISUALIZER; } inline bool IsStatic() const { return _flags & LIGHTMAPONLY_STATIC; } - inline float GetRange() const { return range; } + inline float GetRange() const { return range_global; } inline void SetType(LightType val) { type = val; @@ -792,9 +792,11 @@ namespace wiSceneSystem // Non-serialized attributes: XMFLOAT3 position; - float range; + float range_global; XMFLOAT3 direction; + inline float GetRange() const { return range_global; } + void Serialize(wiArchive& archive, uint32_t seed = 0); }; @@ -881,6 +883,7 @@ namespace wiSceneSystem inline bool IsPlaying() const { return _flags & PLAYING; } inline bool IsLooped() const { return _flags & LOOPED; } inline float GetLength() const { return end - start; } + inline bool IsEnded() const { return timer >= end; } inline void Play() { _flags |= PLAYING; } inline void Pause() { _flags &= ~PLAYING; } diff --git a/WickedEngine/wiSceneSystem_BindLua.cpp b/WickedEngine/wiSceneSystem_BindLua.cpp index 244f28a0f..bcbc0b4f6 100644 --- a/WickedEngine/wiSceneSystem_BindLua.cpp +++ b/WickedEngine/wiSceneSystem_BindLua.cpp @@ -987,6 +987,10 @@ Luna::FunctionType AnimationComponent_BindLua::metho lunamethod(AnimationComponent_BindLua, Play), lunamethod(AnimationComponent_BindLua, Pause), lunamethod(AnimationComponent_BindLua, Stop), + lunamethod(AnimationComponent_BindLua, SetLooped), + lunamethod(AnimationComponent_BindLua, IsLooped), + lunamethod(AnimationComponent_BindLua, IsPlaying), + lunamethod(AnimationComponent_BindLua, IsEnded), { NULL, NULL } }; Luna::PropertyType AnimationComponent_BindLua::properties[] = { @@ -1021,6 +1025,35 @@ int AnimationComponent_BindLua::Stop(lua_State* L) component->Stop(); return 0; } +int AnimationComponent_BindLua::SetLooped(lua_State* L) +{ + int argc = wiLua::SGetArgCount(L); + if (argc > 0) + { + bool looped = wiLua::SGetBool(L, 1); + component->SetLooped(looped); + } + else + { + wiLua::SError(L, "SetLooped(bool value) not enough arguments!"); + } + return 0; +} +int AnimationComponent_BindLua::IsLooped(lua_State* L) +{ + wiLua::SSetBool(L, component->IsLooped()); + return 1; +} +int AnimationComponent_BindLua::IsPlaying(lua_State* L) +{ + wiLua::SSetBool(L, component->IsPlaying()); + return 1; +} +int AnimationComponent_BindLua::IsEnded(lua_State* L) +{ + wiLua::SSetBool(L, component->IsEnded()); + return 1; +} diff --git a/WickedEngine/wiSceneSystem_BindLua.h b/WickedEngine/wiSceneSystem_BindLua.h index a0b341660..34888409d 100644 --- a/WickedEngine/wiSceneSystem_BindLua.h +++ b/WickedEngine/wiSceneSystem_BindLua.h @@ -152,6 +152,10 @@ namespace wiSceneSystem_BindLua int Play(lua_State* L); int Pause(lua_State* L); int Stop(lua_State* L); + int SetLooped(lua_State* L); + int IsLooped(lua_State* L); + int IsPlaying(lua_State* L); + int IsEnded(lua_State* L); }; class MaterialComponent_BindLua diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index 24872fbce..17ebbf58b 100644 --- a/WickedEngine/wiVersion.cpp +++ b/WickedEngine/wiVersion.cpp @@ -9,7 +9,7 @@ namespace wiVersion // minor features, major updates const int minor = 26; // minor bug fixes, alterations, refactors, updates - const int revision = 10; + const int revision = 11; long GetVersion() diff --git a/models/Havoc/baseColor.png b/models/Havoc/baseColor.png new file mode 100644 index 000000000..061e64fff Binary files /dev/null and b/models/Havoc/baseColor.png differ diff --git a/models/Havoc/havoc.wiscene b/models/Havoc/havoc.wiscene new file mode 100644 index 000000000..fa61746b4 Binary files /dev/null and b/models/Havoc/havoc.wiscene differ diff --git a/models/Havoc/normalMap.png b/models/Havoc/normalMap.png new file mode 100644 index 000000000..aa53803bd Binary files /dev/null and b/models/Havoc/normalMap.png differ diff --git a/models/dojo.wiscene b/models/dojo.wiscene new file mode 100644 index 000000000..6c74efa24 Binary files /dev/null and b/models/dojo.wiscene differ diff --git a/scripts/fighting_game.lua b/scripts/fighting_game.lua new file mode 100644 index 000000000..619af6597 --- /dev/null +++ b/scripts/fighting_game.lua @@ -0,0 +1,627 @@ +-- Lua Fighting game sample script +-- +-- README: +-- The script is programmable using common fighting game "numpad notations" (read this if you are unfamiliar: http://www.dustloop.com/wiki/index.php/Notation ) +-- There are four action buttons: A, B, C, D +-- So for example a forward motion combined with action D would look like this in code: "6D" +-- A D action without motion (neutral D) would be: "5D" +-- A quarter circle forward + A would be "236A" +-- "Shoryuken" + A command would be: "623A" +-- For a full circle motion, the input would be: "23698741" +-- But because that full circle motion is difficult to execute properly, we can make it easier by accpeting similar inputs, like: +-- "2684" or "2369874"... + +local scene = GetScene() + +-- The character "class" is a wrapper function that returns a local internal table called "self" +local function Character(face, shirt_color) + local self = { + model = INVALID_ENTITY, + face = 1, -- face direction (X) + request_face = 1, + position = Vector(), + velocity = Vector(), + force = Vector(), + frame = 0, + input_buffer = {}, + + -- Common requirement conditions for state transitions: + require_input_window = function(self, inputString, window) -- player input notation with some tolerance to input execution window (in frames) (help: see readme on top of this file) + for i,element in ipairs(self.input_buffer) do + if(element.age <= window and element.command == inputString) then + return true + end + end + return false + end, + require_input = function(self, inputString) -- player input notation (immediate) (help: see readme on top of this file) + return self:require_input_window(inputString, 0) + end, + require_frame = function(self, frame) -- specific frame + return self.frame == frame + end, + require_window = function(self, frameStart, frameEnd) -- frame window range + return self.frame >= frameStart and self.frame <= frameEnd + end, + require_animationfinish = function(self) -- animation is finished + return scene.Component_GetAnimation(self.states[self.state].anim).IsEnded() + end, + + -- List all possible states: + states = { + Idle = { + anim_name = "Idle", + anim = INVALID_ENTITY, + }, + Walk_Backward = { + anim_name = "Back", + anim = INVALID_ENTITY, + update = function(self) + self.force = vector.Add(self.force, Vector(-0.025 * self.face, 0)) + end, + }, + Walk_Forward = { + anim_name = "Forward", + anim = INVALID_ENTITY, + update = function(self) + self.force = vector.Add(self.force, Vector(0.025 * self.face, 0)) + end, + }, + Jump = { + anim_name = "Jump", + anim = INVALID_ENTITY, + looped = false, + update = function(self) + if(self.frame == 0) then + self.force = vector.Add(self.force, Vector(0, 0.8)) + end + end, + }, + JumpBack = { + anim_name = "Jump", + anim = INVALID_ENTITY, + looped = false, + update = function(self) + if(self.frame == 0) then + self.force = vector.Add(self.force, Vector(-0.2 * self.face, 0.8)) + end + end, + }, + JumpForward = { + anim_name = "Jump", + anim = INVALID_ENTITY, + looped = false, + update = function(self) + if(self.frame == 0) then + self.force = vector.Add(self.force, Vector(0.2 * self.face, 0.8)) + end + end, + }, + FallStart = { + anim_name = "FallStart", + anim = INVALID_ENTITY, + looped = false, + }, + Fall = { + anim_name = "Fall", + anim = INVALID_ENTITY, + }, + FallEnd = { + anim_name = "FallEnd", + anim = INVALID_ENTITY, + looped = false, + }, + CrouchStart = { + anim_name = "CrouchStart", + anim = INVALID_ENTITY, + looped = false, + }, + Crouch = { + anim_name = "Crouch", + anim = INVALID_ENTITY, + }, + CrouchEnd = { + anim_name = "CrouchEnd", + anim = INVALID_ENTITY, + looped = false, + }, + Turn = { + anim_name = "Turn", + anim = INVALID_ENTITY, + looped = false, + update = function(self) + if(self.frame == 0) then + self.face = self.request_face + end + end, + }, + + LightPunch = { + anim_name = "LightPunch", + anim = INVALID_ENTITY, + looped = false, + }, + ForwardLightPunch = { + anim_name = "FLightPunch", + anim = INVALID_ENTITY, + looped = false, + }, + HeavyPunch = { + anim_name = "HeavyPunch", + anim = INVALID_ENTITY, + looped = false, + }, + LowPunch = { + anim_name = "LowPunch", + anim = INVALID_ENTITY, + looped = false, + }, + LightKick = { + anim_name = "LightKick", + anim = INVALID_ENTITY, + looped = false, + }, + HeavyKick = { + anim_name = "HeavyKick", + anim = INVALID_ENTITY, + looped = false, + }, + LowKick = { + anim_name = "LowKick", + anim = INVALID_ENTITY, + looped = false, + }, + Uppercut = { + anim_name = "Uppercut", + anim = INVALID_ENTITY, + looped = false, + }, + Shoryuken = { + anim_name = "Shoryuken", + anim = INVALID_ENTITY, + looped = false, + update = function(self) + if(self:require_frame(0)) then + self.force = vector.Add(self.force, Vector(0.3 * self.face, 0.9)) + end + end, + }, + }, + + -- State machine describes all possible state transitions: + -- StateFrom = { + -- { "StateTo1", condition = function(self) return [requirements that should be met] end }, + -- { "StateTo2", condition = function(self) return [requirements that should be met] end }, + -- } + statemachine = { + Idle = { + { "Turn", condition = function(self) return self.request_face ~= self.face and self:require_input("5") end, }, + { "Walk_Forward", condition = function(self) return self:require_input("6") end, }, + { "Walk_Backward", condition = function(self) return self:require_input("4") end, }, + { "Jump", condition = function(self) return self:require_input("8") end, }, + { "JumpBack", condition = function(self) return self:require_input("7") end, }, + { "JumpForward", condition = function(self) return self:require_input("9") end, }, + { "CrouchStart", condition = function(self) return self:require_input("1") or self:require_input("2") or self:require_input("3") end, }, + { "LightPunch", condition = function(self) return self:require_input("5A") end, }, + { "HeavyPunch", condition = function(self) return self:require_input("5B") end, }, + { "LightKick", condition = function(self) return self:require_input("5C") end, }, + }, + Walk_Backward = { + { "CrouchStart", condition = function(self) return self:require_input("1") or self:require_input("2") or self:require_input("3") end, }, + { "Walk_Forward", condition = function(self) return self:require_input("6") end, }, + { "JumpBack", condition = function(self) return self:require_input("7") end, }, + { "Idle", condition = function(self) return self:require_input("5") end, }, + { "LightPunch", condition = function(self) return self:require_input("5A") end, }, + { "HeavyPunch", condition = function(self) return self:require_input("5B") end, }, + { "LightKick", condition = function(self) return self:require_input("5C") end, }, + { "ForwardLightPunch", condition = function(self) return self:require_input("6A") end, }, + { "HeavyKick", condition = function(self) return self:require_input("6C") end, }, + }, + Walk_Forward = { + { "CrouchStart", condition = function(self) return self:require_input("1") or self:require_input("2") or self:require_input("3") end, }, + { "Walk_Backward", condition = function(self) return self:require_input("4") end, }, + { "JumpForward", condition = function(self) return self:require_input("9") end, }, + { "Idle", condition = function(self) return self:require_input("5") end, }, + { "LightPunch", condition = function(self) return self:require_input("5A") end, }, + { "HeavyPunch", condition = function(self) return self:require_input("5B") end, }, + { "LightKick", condition = function(self) return self:require_input("5C") end, }, + { "ForwardLightPunch", condition = function(self) return self:require_input("6A") end, }, + { "HeavyKick", condition = function(self) return self:require_input("6C") end, }, + }, + Jump = { + { "FallStart", condition = function(self) return self.velocity.GetY() <= 0 end, }, + }, + JumpForward = { + { "FallStart", condition = function(self) return self.velocity.GetY() <= 0 end, }, + }, + JumpBack = { + { "FallStart", condition = function(self) return self.velocity.GetY() <= 0 end, }, + }, + FallStart = { + { "FallEnd", condition = function(self) return self.position.GetY() <= 0.5 end, }, + { "Fall", condition = function(self) return self:require_animationfinish() end, }, + }, + Fall = { + { "FallEnd", condition = function(self) return self.position.GetY() <= 0.5 end, }, + }, + FallEnd = { + { "Idle", condition = function(self) return self.position.GetY() <= 0 and self:require_animationfinish() end, }, + }, + CrouchStart = { + { "Idle", condition = function(self) return self:require_input("5") end, }, + { "Crouch", condition = function(self) return (self:require_input("1") or self:require_input("2") or self:require_input("3")) and self:require_animationfinish() end, }, + }, + Crouch = { + { "CrouchEnd", condition = function(self) return self:require_input("5") or self:require_input("4") or self:require_input("6") or self:require_input("7") or self:require_input("8") or self:require_input("9") end, }, + { "LowPunch", condition = function(self) return self:require_input("2A") or self:require_input("1A") or self:require_input("3A") end, }, + { "LowKick", condition = function(self) return self:require_input("2C") or self:require_input("1C") or self:require_input("3C") end, }, + { "Uppercut", condition = function(self) return self:require_input("2B") or self:require_input("1B") or self:require_input("3B") end, }, + { "Shoryuken", condition = function(self) return self:require_input("2D") end, }, + }, + CrouchEnd = { + { "Idle", condition = function(self) return self:require_animationfinish() end, }, + }, + Turn = { + { "Idle", condition = function(self) return self:require_animationfinish() end, }, + }, + LightPunch = { + { "Idle", condition = function(self) return self:require_animationfinish() end, }, + }, + ForwardLightPunch = { + { "Idle", condition = function(self) return self:require_animationfinish() end, }, + }, + HeavyPunch = { + { "Idle", condition = function(self) return self:require_animationfinish() end, }, + }, + LowPunch = { + { "Crouch", condition = function(self) return self:require_animationfinish() end, }, + }, + LightKick = { + { "Idle", condition = function(self) return self:require_animationfinish() end, }, + }, + HeavyKick = { + { "Idle", condition = function(self) return self:require_animationfinish() end, }, + }, + LowKick = { + { "Crouch", condition = function(self) return self:require_animationfinish() end, }, + }, + Uppercut = { + { "Idle", condition = function(self) return self:require_animationfinish() end, }, + }, + Shoryuken = { + { "FallStart", condition = function(self) return self:require_animationfinish() end, }, + }, + }, + + state = "Idle", -- starting state + + + -- Ends the current state: + EndState = function(self) + scene.Component_GetAnimation(self.states[self.state].anim).Stop() + end, + -- Starts a new state: + StartState = function(self, dst_state) + scene.Component_GetAnimation(self.states[dst_state].anim).Play() + self.frame = 0 + self.state = dst_state + end, + -- Parse state machine at current state and perform transition if applicable: + StepStateMachine = function(self) + local transition_candidates = self.statemachine[self.state] + if(transition_candidates ~= nil) then + for i,dst in pairs(transition_candidates) do + -- check transition requirement conditions: + local requirements_met = true + if(dst.condition ~= nil) then + requirements_met = dst.condition(self) + end + if(requirements_met) then + -- transition to new state when all requirements are met: + self:EndState() + self:StartState(dst[1]) + return + end + end + end + end, + -- Execute the currently active state: + ExecuteCurrentState = function(self) + local current_state = self.states[self.state] + if(current_state ~= nil) then + if(current_state.update ~= nil) then + current_state.update(self) + end + end + end, + + + Create = function(self, face, shirt_color) + + -- Load the model into a custom scene: + -- We use a custom scene because if two models are loaded into the global scene, they will have name collisions + -- and thus we couldn't properly query entities by name + local model_scene = Scene() + self.model = LoadModel(model_scene, "../models/havoc/havoc.wiscene") + + -- Place model according to starting facing direction: + self.face = face + self.request_face = face + self.position = Vector(self.face * -4) + + -- Set shirt color todifferentiate between characters: + local shirt_material_entity = model_scene.Entity_FindByName("material_shirt") + model_scene.Component_GetMaterial(shirt_material_entity).SetBaseColor(shirt_color) + + -- Initialize states: + for i,state in pairs(self.states) do + state.anim = model_scene.Entity_FindByName(state.anim_name) + if(state.looped ~= nil) then + model_scene.Component_GetAnimation(state.anim).SetLooped(state.looped) + end + end + + -- Move the custom scene into the global scene: + scene.Merge(model_scene) + + self:StartState(self.state) + + end, + + AI = function(self) + -- todo some AI bot behaviour + table.insert(self.input_buffer, {age = 0, command = "5"}) + end, + + Input = function(self) + + local input_string = "" + + -- read input: + local left = input.Down(string.byte('A')) + local right = input.Down(string.byte('D')) + local up = input.Down(string.byte('W')) + local down = input.Down(string.byte('S')) + local A = input.Press(VK_RIGHT) + local B = input.Press(VK_UP) + local C = input.Press(VK_LEFT) + local D = input.Press(VK_DOWN) + + -- swap left and right if facing is opposite side: + if(self.face < 0) then + local tmp = right + right = left + left = tmp + end + + if(up and left) then + input_string = "7" + elseif(up and right) then + input_string = "9" + elseif(up) then + input_string = "8" + elseif(down and left) then + input_string = "1" + elseif(down and right) then + input_string = "3" + elseif(down) then + input_string = "2" + elseif(left) then + input_string = "4" + elseif(right) then + input_string = "6" + else + input_string = "5" + end + + if(A) then + input_string = input_string .. "A" + end + if(B) then + input_string = input_string .. "B" + end + if(C) then + input_string = input_string .. "C" + end + if(D) then + input_string = input_string .. "D" + end + + + table.insert(self.input_buffer, {age = 0, command = input_string}) + + end, + + Update = function(self) + self.frame = self.frame + 1 + + self:StepStateMachine() + self:ExecuteCurrentState() + + -- Manage input buffer: + for i,element in pairs(self.input_buffer) do + element.age = element.age + 1 + end + if(#self.input_buffer > 120) then + table.remove(self.input_buffer, 1) + end + + -- force from gravity: + self.force = vector.Add(self.force, Vector(0,-0.04,0)) + + -- apply force: + self.velocity = vector.Add(self.velocity, self.force) + self.force = Vector() + + -- aerial drag: + self.velocity = vector.Multiply(self.velocity, 0.98) + + -- apply velocity: + self.position = vector.Add(self.position, self.velocity) + + -- check if we are below or on the ground: + if(self.position.GetY() <= 0 and self.velocity.GetY()<=0) then + self.position.SetY(0) -- snap to ground + self.velocity.SetY(0) -- don't fall below ground + self.velocity = vector.Multiply(self.velocity, 0.8) -- ground drag: + end + + -- Transform component gets set as absolute coordinates every frame: + local model_transform = scene.Component_GetTransform(self.model) + model_transform.ClearTransform() + model_transform.Translate(self.position) + model_transform.Rotate(Vector(0, 3.1415 * ((self.face - 1) * 0.5))) + model_transform.UpdateTransform() + + -- Some debug draw: + DrawPoint(model_transform.GetPosition(), 0.1, Vector(1,0,0,1)) + DrawLine(model_transform.GetPosition(),model_transform.GetPosition():Add(self.velocity), Vector(0,1,0,1)) + DrawLine(model_transform.GetPosition(),model_transform.GetPosition():Add(Vector(self.face)), Vector(0,0,1,1)) + + end + + } + + self:Create(face, shirt_color) + return self +end + + +-- script camera state: +local camera_position = Vector() +local camera_transform = TransformComponent() + +-- Interaction between two characters: +local ResolveCharacters = function(player1, player2) + + -- Facing direction requests: + if(player1.position.GetX() < player2.position.GetX()) then + player1.request_face = 1 + player2.request_face = -1 + else + player1.request_face = -1 + player2.request_face = 1 + end + + -- Camera: + local CAMERA_HEIGHT = 4 -- camera height from ground + local DEFAULT_CAMERADISTANCE = -9.5 -- the default camera distance when characters are close to each other + local MODIFIED_CAMERADISTANCE = -11.5 -- if the two players are far enough from each other, the camera will zoom out to this distance + local CAMERA_DISTANCE_MODIFIER = 10 -- the required distance between the characters when the camera should zoom out + local XBOUNDS = 20 -- play area horizontal bounds + local CAMERA_SIDE_LENGTH = 11 -- play area inside the camera (character can't move outside camera even if inside the play area) + + -- Clamp the players inside the camera: + local camera_side_left = camera_position.GetX() - CAMERA_SIDE_LENGTH + local camera_side_right = camera_position.GetX() + CAMERA_SIDE_LENGTH + player1.position.SetX(math.clamp(player1.position.GetX(), camera_side_left, camera_side_right)) + player2.position.SetX(math.clamp(player2.position.GetX(), camera_side_left, camera_side_right)) + + local camera_position_new = Vector() + local distanceX = math.abs(player1.position.GetX() - player2.position.GetX()) + local distanceY = math.abs(player1.position.GetY() - player2.position.GetY()) + + -- camera height: + if(player1.position.GetY() > 4 or player2.position.GetY() > 4) then + camera_position_new.SetY( math.min(player1.position.GetY(), player2.position.GetY()) + distanceY ) + else + camera_position_new.SetY(CAMERA_HEIGHT) + end + + -- camera distance: + if(distanceX > CAMERA_DISTANCE_MODIFIER) then + camera_position_new.SetZ(MODIFIED_CAMERADISTANCE) + else + camera_position_new.SetZ(DEFAULT_CAMERADISTANCE) + end + + -- camera horizontal position: + local centerX = math.clamp((player1.position.GetX() + player2.position.GetX()) * 0.5, -XBOUNDS, XBOUNDS) + camera_position_new.SetX(centerX) + + -- smooth camera: + camera_position = vector.Lerp(camera_position, camera_position_new, 0.1) + + -- finally update the global camera with current values: + camera_transform.ClearTransform() + camera_transform.Translate(camera_position) + camera_transform.UpdateTransform() + GetCamera().TransformCamera(camera_transform) + +end + +-- Main loop: +runProcess(function() + + ClearWorld() + + -- Fighting game needs stable frame rate and deterministic controls at all times. We will also refer to frames in this script instead of time units. + -- We lock the framerate to 60 FPS, so if frame rate goes below, game will play slower + -- + -- There is also the possibility to implement game logic in fixed_update() instead, but that is not common for fighting games + main.SetTargetFrameRate(60) + main.SetFrameRateLock(true) + + -- We will override the render path so we can invoke the script from Editor and controls won't collide with editor scripts + -- Also save the active component that we can restore when ESCAPE is pressed + local prevPath = main.GetActivePath() + local path = RenderPath3D_TiledForward() + main.SetActivePath(path) + + local font = Font("This script is showcasing how to write a simple fighting game.\nControls:\n#####################\nWASD: move\narrows: actions (ABCD)\nESCAPE key: quit\nR: reload script"); + font.SetSize(20) + font.SetPos(Vector(10, GetScreenHeight() - 10)) + font.SetAlign(WIFALIGN_LEFT, WIFALIGN_BOTTOM) + font.SetColor(0xFFADA3FF) + font.SetShadowColor(Vector(0,0,0,1)) + path.AddFont(font) + + local info = Font(""); + info.SetSize(20) + info.SetPos(Vector(GetScreenWidth() / 2, GetScreenHeight() * 0.9)) + info.SetAlign(WIFALIGN_LEFT, WIFALIGN_CENTER) + info.SetShadowColor(Vector(0,0,0,1)) + path.AddFont(info) + + LoadModel("../models/dojo.wiscene") + + -- Create the two player characters. Parameters are facing direction and shirt material color to differentiate between them: + local player1 = Character(1, Vector(1,1,1,1)) -- facing to right, white shirt + local player2 = Character(-1, Vector(1,0,0,1)) -- facing to left, red shirt + + while true do + + player1:Input() + player2:AI() + + player1:Update() + player2:Update() + + ResolveCharacters(player1, player2) + + info.SetText("state = " .. player1.state .. "\nframe = " .. player1.frame) + + -- Wait for Engine update tick + update() + + + if(input.Press(VK_ESCAPE)) then + -- restore previous component + -- so if you loaded this script from the editor, you can go back to the editor with ESC + backlog_post("EXIT") + killProcesses() + main.SetActivePath(prevPath) + return + end + if(input.Press(string.byte('R'))) then + -- reload script + backlog_post("RELOAD") + killProcesses() + main.SetActivePath(prevPath) + dofile("fighting_game.lua") + return + end + + end +end) +