#include #include #include "GameData.h" #include "WaterModule.h" #include "TerrainModule.h" #include "Components.h" #include "PhysicsModule.h" #include "physics.h" #include "CharacterAnimationModule.h" #include "CharacterManagerModule.h" #include "CharacterModule.h" #include namespace ECS { CharacterModule::CharacterModule(flecs::world &ecs) { ZoneScoped; struct TriggerPhysicsChange {}; static std::vector part_names; const std::vector &groups = Ogre::ResourceGroupManager::getSingleton().getResourceGroups(); if (part_names.size() == 0) { int i; for (i = 0; i < groups.size(); i++) { std::vector names = *Ogre::ResourceGroupManager::getSingleton() .findResourceNames(groups[i], "body_part_*.json"); part_names.insert(part_names.end(), names.begin(), names.end()); } } body_parts = nlohmann::json::object(); for (auto &g : part_names) { Ogre::String group = Ogre::ResourceGroupManager::getSingleton() .findGroupContainingResource(g); Ogre::DataStreamPtr stream = Ogre::ResourceGroupManager::getSingleton().openResource( g, group); Ogre::String json = stream->getAsString(); nlohmann::json jdata = nlohmann::json::parse(json); if (jdata.find("age") == jdata.end()) continue; if (jdata.find("sex") == jdata.end()) continue; if (jdata.find("slot") == jdata.end()) continue; if (jdata.find("mesh") == jdata.end()) continue; Ogre::String age = jdata["age"].get(); Ogre::String sex = jdata["sex"].get(); Ogre::String slot = jdata["slot"].get(); Ogre::String mesh = jdata["mesh"].get(); if (body_parts.find(age) == body_parts.end()) body_parts[age] = nlohmann::json::object(); if (body_parts[age].find(sex) == body_parts[age].end()) body_parts[age][sex] = nlohmann::json::object(); if (body_parts[age][sex].find(slot) == body_parts[age][sex].end()) body_parts[age][sex][slot] = nlohmann::json::array(); body_parts[age][sex][slot].push_back(mesh); mesh_names.insert(mesh); Ogre::MeshManager::getSingleton().load(mesh, "Characters"); } std::cout << body_parts.dump(4) << std::endl; ecs.module(); ecs.component(); ecs.component(); ecs.component() .on_remove([this](flecs::entity e, CharacterBase &ch) { ZoneScoped; if (characterEntitiesFace.find(e) != characterEntitiesFace.end() || characterNodes.find(e) != characterNodes.end()) { // FIXME: clean up data characterEntitiesFace.erase(e); characterEntitiesTop.erase(e); characterEntitiesBottom.erase(e); characterEntitiesFeet.erase(e); characterNodes.erase(e); ECS::modified(); } }) .on_add([this](flecs::entity e, CharacterBase &ch) { if (characterNodes.find(e) == characterNodes.end()) { ZoneScoped; OgreAssert(characterModelsFace.find(e) != characterModelsFace.end(), "no model set"); const EngineData &eng = ECS::get(); Ogre::SceneNode *bodyNode = eng.mScnMgr->getRootSceneNode() ->createChildSceneNode(); Ogre::Entity *faceEnt = eng.mScnMgr->createEntity( characterModelsFace[e]); bodyNode->attachObject(faceEnt); characterNodes[e] = bodyNode; characterEntitiesFace[e] = faceEnt; Ogre::Entity *hairEnt = eng.mScnMgr->createEntity( characterModelsHair[e]); hairEnt->shareSkeletonInstanceWith(faceEnt); bodyNode->attachObject(hairEnt); characterEntitiesHair[e] = hairEnt; Ogre::Entity *topEnt = eng.mScnMgr->createEntity( characterModelsTop[e]); topEnt->shareSkeletonInstanceWith(faceEnt); bodyNode->attachObject(topEnt); characterEntitiesTop[e] = topEnt; Ogre::Entity *bottomEnt = eng.mScnMgr->createEntity( characterModelsBottom[e]); bottomEnt->shareSkeletonInstanceWith(faceEnt); bodyNode->attachObject(bottomEnt); characterEntitiesBottom[e] = bottomEnt; Ogre::Entity *feetEnt = eng.mScnMgr->createEntity( characterModelsFeet[e]); feetEnt->shareSkeletonInstanceWith(faceEnt); bodyNode->attachObject(feetEnt); characterEntitiesFeet[e] = feetEnt; #if 0 if (characterModelsTop.find(e) != characterModelsTop.end()) { Ogre::String skeletonName = bodyEnt->getMesh() ->getSkeletonName(); Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton() .load(characterModelsTop [e], "General"); Ogre::String mname = mesh->getName(); mesh = mesh->clone(mname + "_clone"); OgreAssert( mesh, "No mesh " + characterModelsTop[e]); Ogre::String clothSkeleton = mesh->getSkeletonName(); if (clothSkeleton != skeletonName) { mesh->setSkeletonName( skeletonName); mesh->load(); if (Ogre::SkeletonManager::getSingleton() .resourceExists( clothSkeleton)) Ogre::SkeletonManager:: getSingleton() .remove(clothSkeleton); } Ogre::Entity *characterTop = eng.mScnMgr->createEntity(mesh); characterTop->shareSkeletonInstanceWith( bodyEnt); bodyNode->attachObject(characterTop); } #endif ECS::modified(); } OgreAssert(characterOrientations.find(e) != characterOrientations.end(), "Bad orientation/position"); Ogre::SceneNode *bodyNode = characterNodes[e]; bodyNode->setOrientation(characterOrientations[e]); bodyNode->setPosition(characterPositions[e]); OgreAssert( characterEntitiesFace[e]->getSkeleton()->hasBone( "Root"), "No root bone"); ch.mBoneMotion = Ogre::Vector3::ZERO; ch.mBonePrevMotion = Ogre::Vector3::ZERO; }); ecs.component(); ecs.component().on_set( [this](flecs::entity e, CharacterLocation &loc) { ZoneScoped; characterOrientations[e] = loc.orientation; characterPositions[e] = loc.position; ECS::modified(); }); ecs.component(); ecs.component(); ecs.component(); ecs.component(); ecs.component(); ecs.component(); ecs.component(); ecs.import (); ecs.import (); ecs.import (); ecs.system("UpdateTimer") .kind(flecs::OnUpdate) .each([this](EngineData &eng, CharacterBase &ch) { ZoneScopedN("UpdateTimer"); ch.mTimer += eng.delta; }); ecs.system("HandleInput") .kind(flecs::OnUpdate) .each([this](Input &input, Camera &camera) { ZoneScopedN("HandleInput"); flecs::entity player = ECS::get().getPlayer(); if (!player.is_valid()) return; /* handle input */ // if (input.control == input.control_prev) // return; uint32_t pressed = input.control & ~input.control_prev; uint32_t released = input.control_prev & ~input.control; uint32_t active = input.control; float zaxis = input.motion.z; zaxis *= 0.9f; if (!camera.mCameraPivot || !camera.mCameraGoal) return; if (pressed & 1) std::cout << "W pressed\n"; if (released & 1) std::cout << "W released\n"; if (active & 1) zaxis -= 1.0f; if (active & 4) zaxis += 1.0f; if (zaxis > -1.0f && zaxis < 1.0f) zaxis = 0.0f; else zaxis = Ogre::Math::Sign(zaxis); if (active & 32) input.act = true; else input.act = false; if (pressed & 32) input.act_pressed = true; else input.act_pressed = false; if (active & 64) input.act2 = true; else input.act2 = false; if (pressed & 64) input.act2_pressed = true; else input.act2_pressed = false; input.motion.z = zaxis; float xaxis = input.motion.x; xaxis *= 0.9f; if (active & 2) xaxis = -1.0f; if (active & 8) xaxis += 1.0f; if (xaxis > -1.0f && xaxis < 1.0f) xaxis = 0.0f; else xaxis = Ogre::Math::Sign(xaxis); input.motion.x = xaxis; if (active & 16) input.fast = true; else input.fast = false; input.control_prev = input.control; float a = 0, b = 0, c = 0; if (input.mouse_moved) { a += -0.18f * input.mouse.x; b += -0.12f * input.mouse.y; input.mouse_moved = false; input.mouse.x = 0; input.mouse.y = 0; updateCameraGoal(camera, a, b, c); } if (input.wheel_moved) { c += -0.15f * input.wheel_y; input.wheel_moved = false; input.wheel_y = 0; updateCameraGoal(camera, a, b, c); } ECS::get().modified(); }); #define TURN_SPEED 500.0f // character turning in degrees per second ecs.system("UpdateBody") .kind(flecs::OnUpdate) .with() .with() .without() .without() .each([](flecs::entity e, const Input &input, const Camera &camera, CharacterBase &ch) { ZoneScopedN("UpdateBody"); ch.mGoalDirection = Ogre::Vector3::ZERO; float delta = e.world().delta_time(); if (!input.motion.zeroLength()) { // calculate actually goal direction in world based on player's key directions ch.mGoalDirection += input.motion.z * camera.mCameraNode->getOrientation() .zAxis(); ch.mGoalDirection += input.motion.x * camera.mCameraNode->getOrientation() .xAxis(); ch.mGoalDirection.y = 0; ch.mGoalDirection.normalise(); Ogre::Quaternion toGoal = ECS::get() .characterNodes.at(e) ->getOrientation() .zAxis() .getRotationTo( ch.mGoalDirection); // calculate how much the character has to turn to face goal direction Ogre::Real yawToGoal = toGoal.getYaw().valueDegrees(); // this is how much the character CAN turn this frame Ogre::Real yawAtSpeed = yawToGoal / Ogre::Math::Abs(yawToGoal) * delta * TURN_SPEED; // reduce "turnability" if we're in midair // if (mBaseAnimID == ANIM_JUMP_LOOP) yawAtSpeed *= 0.2f; if (yawToGoal < 0) yawToGoal = std::min( 0, std::max( yawToGoal, yawAtSpeed)); //yawToGoal = Math::Clamp(yawToGoal, yawAtSpeed, 0); else if (yawToGoal > 0) yawToGoal = std::max( 0, std::min( yawToGoal, yawAtSpeed)); //yawToGoal = Math::Clamp(yawToGoal, 0, yawAtSpeed); ECS::get() .characterNodes.at(e) ->yaw(Ogre::Degree(yawToGoal)); } }); #if 0 ecs.system("UpdateCharacterBase") .kind(flecs::OnUpdate) .with() .with() .with() .each([](const EngineData &eng, CharacterLocation &loc, CharacterBase &ch, CharacterBody &body) { if (!ch.mBodyNode) { } else { loc.orientation = ch.mBodyNode->_getDerivedOrientation(); loc.position = ch.mBodyNode->_getDerivedPosition(); } }); #endif static int characterCount = 0; ecs.observer("SetupAnimationControlObs") .event(flecs::OnAdd) .each([](flecs::entity e, AnimationControl &anim) { anim.configured = false; }); #define CAM_HEIGHT 1.6f // height of camera above character's center of mass ecs.system( "UpdateCamera") .kind(flecs::OnUpdate) .with() .with() .each([](flecs::entity e, const EngineData &eng, Camera &camera, const CharacterBase &ch) { ZoneScopedN("UpdateCamera"); float delta = eng.delta; if (!camera.configured) { // create a pivot at roughly the character's shoulder camera.mCameraPivot = eng.mScnMgr->getRootSceneNode() ->createChildSceneNode(); camera.mCameraGoal = camera.mCameraPivot ->createChildSceneNode( Ogre::Vector3(0, 2, 3)); camera.mCameraNode->setPosition( camera.mCameraPivot->getPosition() + camera.mCameraGoal->getPosition()); camera.mCameraPivot->setFixedYawAxis(true); camera.mCameraGoal->setFixedYawAxis(true); camera.mCameraNode->setFixedYawAxis(true); // our model is quite small, so reduce the clipping planes camera.mCamera->setNearClipDistance(0.1f); camera.mCamera->setFarClipDistance(700); camera.mPivotPitch = 0; camera.configured = true; } else { // place the camera pivot roughly at the character's shoulder camera.mCameraPivot->setPosition( ECS::get() .characterNodes.at(e) ->getPosition() + Ogre::Vector3::UNIT_Y * CAM_HEIGHT); // move the camera smoothly to the goal Ogre::Vector3 goalOffset = camera.mCameraGoal ->_getDerivedPosition() - camera.mCameraNode->getPosition(); camera.mCameraNode->translate(goalOffset * delta * 9.0f); // always look at the pivot camera.mCameraNode->lookAt( camera.mCameraPivot ->_getDerivedPosition(), Ogre::Node::TS_PARENT); } }); #if 0 class ClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback { btCollisionObject *mMe; public: ClosestNotMeRayResultCallback(btCollisionObject *me, const btVector3 &from, const btVector3 &to) : btCollisionWorld::ClosestRayResultCallback(from, to) { mMe = me; } virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult &rayResult, bool normalInWorldSpace) { if (rayResult.m_collisionObject == mMe) return 1.0f; return ClosestRayResultCallback::addSingleResult( rayResult, normalInWorldSpace); } }; #endif ecs.system("CheckGround") .kind(flecs::OnUpdate) .with() .with() .without() .each([](const EngineData &eng, CharacterBase &ch) { ZoneScopedN("CheckGround"); #if 0 if (body.mGhostObject) { btVector3 from = body.mGhostObject->getWorldTransform() .getOrigin() + btVector3(0, 0.2f, 0); btVector3 to = from + btVector3(0, -3000.0f, 0); ClosestNotMeRayResultCallback resultCallback( body.mGhostObject, from, to); body.mGhostObject->rayTest(from, to, resultCallback); body.checkGroundResult = resultCallback.hasHit(); if (resultCallback.hasHit()) ECS::get().add(); } #endif ECS::get().add(); }); #if 0 ecs.system( "CharacterWater1") .kind(flecs::OnUpdate) .with() .without() .each([](flecs::entity e, const WaterBody &waterb, const CharacterBase &ch, CharacterBody &body) { #if 0 if (waterb.isInWater(body.mGhostObject) && ch.mBodyNode->_getDerivedPosition().y < -0.05f) { e.add(); std::cout << "Big Splash\n"; } #endif #if 0 if (waterb.mInWater.find(body.mGhostObject) == waterb.mInWater.end()) e.add(); std::cout << waterb.mInWater.size() << " InWater\n"; #endif }); #endif #if 0 ecs.system( "CharacterWater2") .kind(flecs::OnUpdate) .with() .with() .each([](flecs::entity e, const WaterBody &waterb, const CharacterBase &ch, CharacterBody &body) { float h = ch.mBodyNode->_getDerivedPosition().y; #if 0 if (waterb.isInWater(body.mGhostObject) && h > 0.05f) e.remove(); else if (!waterb.isInWater(body.mGhostObject) && h > 0.05f) e.remove(); #endif }); #endif #if 0 ecs.system( "DisplayPlayerPos") .kind(flecs::OnUpdate) .with() .with() .each([](const EngineData &eng, CharacterBase &ch, CharacterBody &body) { std::cout << "player: " << ch.mBodyNode->getPosition() << "\n"; }); #endif #if 0 #endif } void CharacterModule::updateCameraGoal(Camera &camera, Ogre::Real deltaYaw, Ogre::Real deltaPitch, Ogre::Real deltaZoom) { ZoneScoped; static float canonDist = 0; camera.mCameraPivot->yaw(Ogre::Degree(deltaYaw), Ogre::Node::TS_PARENT); if (!(camera.mPivotPitch + deltaPitch > 25 && deltaPitch > 0) && !(camera.mPivotPitch + deltaPitch < -60 && deltaPitch < 0)) { camera.mCameraPivot->pitch(Ogre::Degree(deltaPitch), Ogre::Node::TS_LOCAL); camera.mPivotPitch += deltaPitch; } Ogre::Real dist = camera.mCameraGoal->_getDerivedPosition().distance( camera.mCameraPivot->_getDerivedPosition()); Ogre::Real distChange = deltaZoom * dist; // bound the zoom if (!(dist + distChange < 1.5f && distChange < 0) && !(dist + distChange > 10 && distChange > 0)) { camera.mCameraGoal->translate(0, 0, distChange, Ogre::Node::TS_LOCAL); canonDist += distChange; } JPH::BodyID id; Ogre::Vector3 position; Ogre::Vector3 d = (camera.mCameraPivot->_getDerivedPosition() - camera.mCameraGoal->_getDerivedPosition()) .normalisedCopy(); if (JoltPhysicsWrapper::getSingleton().raycastQuery( camera.mCameraPivot->_getDerivedPosition(), camera.mCameraGoal->_getDerivedPosition() - d * 0.6, position, id)) { float l = camera.mCameraPivot->_getDerivedPosition() .squaredDistance( camera.mCameraGoal ->_getDerivedPosition()); float m = camera.mCameraPivot->_getDerivedPosition() .squaredDistance(position); if (m < l) camera.mCameraGoal->_setDerivedPosition(position + d * 0.6f); } else { Ogre::Real dist2 = camera.mCameraGoal->_getDerivedPosition().distance( camera.mCameraPivot->_getDerivedPosition()); if (deltaZoom < 0.0f || deltaZoom > 0.0f) canonDist = dist2; else { if (canonDist < dist2) canonDist = dist2; if (dist2 < canonDist) camera.mCameraGoal->translate( 0, 0, 0.08f, Ogre::Node::TS_LOCAL); } } } void CharacterModule::createCharacter( flecs::entity e, const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, const Ogre::String &faceModel, const Ogre::String &hairModel, const Ogre::String &topModel, const Ogre::String &bottomModel, const Ogre::String &feetModel) { ZoneScoped; if (e.has() || e.has()) return; if (characterNodes.find(e) != characterNodes.end()) return; e.set({ rotation, position }); characterOrientations[e] = rotation; characterPositions[e] = position; characterModelsFace[e] = faceModel; characterModelsHair[e] = hairModel; characterModelsTop[e] = topModel; characterModelsBottom[e] = bottomModel; characterModelsFeet[e] = feetModel; e.set( { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }); e.add(); e.add(); e.add(); e.add(); e.add(); } void applyWeightBasedScale(Ogre::Entity *ent, const Ogre::String &targetBoneName, const Ogre::Vector3 &scale) { ZoneScoped; Ogre::MeshPtr mesh = ent->getMesh(); Ogre::SkeletonInstance *skel = ent->getSkeleton(); Ogre::Bone *targetBone = skel->getBone(targetBoneName); // Create a non-uniform scale matrix relative to the bone's local space Ogre::Matrix4 scaleMatrix = Ogre::Matrix4::IDENTITY; scaleMatrix.setScale(scale); for (unsigned short i = 0; i < mesh->getNumSubMeshes(); ++i) { Ogre::SubMesh *submesh = mesh->getSubMesh(i); Ogre::VertexData *vertexData = submesh->useSharedVertices ? mesh->sharedVertexData : submesh->vertexData; // 1. Get Position Element const Ogre::VertexElement *posElem = vertexData->vertexDeclaration->findElementBySemantic( Ogre::VES_POSITION); Ogre::HardwareVertexBufferSharedPtr vbuf = vertexData->vertexBufferBinding->getBuffer( posElem->getSource()); // 2. Access Bone Assignments // This map tells us which bones influence which vertex and by how much const Ogre::Mesh::VertexBoneAssignmentList &vbal = submesh->getBoneAssignments(); // Lock buffer for reading and writing float *pVertices = static_cast( vbuf->lock(Ogre::HardwareBuffer::HBL_NORMAL)); for (size_t vIdx = 0; vIdx < vertexData->vertexCount; ++vIdx) { float totalWeight = 0.0f; // Find assignments for this specific vertex for (auto const &[index, assignment] : vbal) { if (assignment.vertexIndex == vIdx && assignment.boneIndex == targetBone->getHandle()) { totalWeight = assignment.weight; break; } } if (totalWeight > 0.0f) { float *pPos; posElem->baseVertexPointerToElement( reinterpret_cast( pVertices) + vIdx * vbuf->getVertexSize(), &pPos); Ogre::Vector3 vertexPos(pPos[0], pPos[1], pPos[2]); // Transform vertex to bone local space, scale it, then move back // This ensures scaling happens along the bone's axis, not the world axis Ogre::Vector3 localPos = targetBone->_getDerivedOrientation() .Inverse() * (vertexPos - targetBone->_getDerivedPosition()); Ogre::Vector3 scaledLocalPos = scaleMatrix * localPos; Ogre::Vector3 worldPos = (targetBone->_getDerivedOrientation() * scaledLocalPos) + targetBone->_getDerivedPosition(); // Interpolate based on weight (Lerp) to handle vertices shared with other bones Ogre::Vector3 finalPos = Ogre::Math::lerp( vertexPos, worldPos, totalWeight); pPos[0] = finalPos.x; pPos[1] = finalPos.y; pPos[2] = finalPos.z; } } vbuf->unlock(); } } void CharacterModule::remapMeshToMasterSkeleton(Ogre::MeshPtr clothMesh, Ogre::MeshPtr masterMesh) { Ogre::SkeletonPtr masterSkel = masterMesh->getSkeleton(); Ogre::SkeletonPtr clothSkel = clothMesh->getSkeleton(); if (!masterSkel || !clothSkel) return; // 1. Create a Lookup Table: ClothIndex -> MasterIndex std::map indexMap; for (unsigned short i = 0; i < clothSkel->getNumBones(); ++i) { Ogre::String boneName = clothSkel->getBone(i)->getName(); if (masterSkel->hasBone(boneName)) { indexMap[i] = masterSkel->getBone(boneName)->getHandle(); } else { indexMap[i] = 0; // Fallback to root } } // 2. Update the Hardware Buffers for each SubMesh for (unsigned short i = 0; i < clothMesh->getNumSubMeshes(); ++i) { Ogre::SubMesh *sub = clothMesh->getSubMesh(i); Ogre::VertexData *vdata = sub->useSharedVertices ? clothMesh->sharedVertexData : sub->vertexData; // Find the element containing bone indices (VES_BLEND_INDICES) const Ogre::VertexElement *idxElem = vdata->vertexDeclaration->findElementBySemantic( Ogre::VES_BLEND_INDICES); if (!idxElem) continue; Ogre::HardwareVertexBufferSharedPtr vbuf = vdata->vertexBufferBinding->getBuffer( idxElem->getSource()); unsigned char *vertex = static_cast( vbuf->lock(Ogre::HardwareBuffer::HBL_NORMAL)); for (size_t j = 0; j < vdata->vertexCount; ++j) { unsigned char *pIndices; idxElem->baseVertexPointerToElement(vertex, &pIndices); // Remap the 4 indices (Ogre hardware skinning usually uses 4 bytes) for (int k = 0; k < 4; ++k) { pIndices[k] = static_cast( indexMap[pIndices[k]]); } vertex += vbuf->getVertexSize(); } vbuf->unlock(); } // 3. Link to Master Skeleton and rebuild clothMesh->setSkeletonName(masterSkel->getName()); clothMesh->_compileBoneAssignments(); } void CharacterModule::getSlotMeshes(const Ogre::String &age, const Ogre::String &sex, const Ogre::String &slotName, std::vector &meshes) { OgreAssert(body_parts.find(age) != body_parts.end(), "bad age: " + age); OgreAssert(body_parts[age].find(sex) != body_parts[age].end(), "bad sex: " + sex); OgreAssert(body_parts[age][sex].find(slotName) != body_parts[age][sex].end(), "bad slot: " + slotName); for (auto &slots : body_parts[age][sex][slotName]) meshes.push_back(slots.get()); } void CharacterModule::preloadMeshes() { for (const auto &mesh : mesh_names) { Ogre::Entity *ent = ECS::get().mScnMgr->createEntity(mesh); ECS::get().mScnMgr->destroyEntity(ent); } } }