Implement footstep sounds

This commit is contained in:
Vsevolod Kremianskii 2021-05-04 23:21:18 +07:00
parent 7a77cabf4c
commit e3d49fd3b2
19 changed files with 268 additions and 21 deletions

View file

@ -289,6 +289,7 @@ set_target_properties(libvideo PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINAR
## libscene static library
set(SCENE_HEADERS
src/scene/animation/eventlistener.h
src/scene/animation/channel.h
src/scene/animation/properties.h
src/scene/animation/scenenodeanimator.h
@ -429,6 +430,7 @@ set(GAME_HEADERS
src/game/enginetype/event.h
src/game/enginetype/location.h
src/game/enginetype/talent.h
src/game/footstepsounds.h
src/game/game.h
src/game/gameidutil.h
src/game/gui/barkbubble.h
@ -535,6 +537,7 @@ set(GAME_SOURCES
src/game/d20/classes.cpp
src/game/d20/skills.cpp
src/game/dialog.cpp
src/game/footstepsounds.cpp
src/game/game.cpp
src/game/game_kotor.cpp
src/game/game_save.cpp

View file

@ -21,6 +21,8 @@
#include <functional>
#include <stdexcept>
#include "glm/gtx/norm.hpp"
#include "AL/al.h"
#include "../common/log.h"
@ -34,6 +36,9 @@ namespace reone {
namespace audio {
static constexpr float kMaxPositionalSoundDistance = 16.0f;
static constexpr float kMaxPositionalSoundDistance2 = kMaxPositionalSoundDistance * kMaxPositionalSoundDistance;
AudioPlayer &AudioPlayer::instance() {
static AudioPlayer instance;
return instance;
@ -155,8 +160,11 @@ void AudioPlayer::enqueue(const shared_ptr<SoundInstance> &sound) {
}
shared_ptr<SoundHandle> AudioPlayer::play(const shared_ptr<AudioStream> &stream, AudioType type, bool loop, float gain, bool positional, glm::vec3 position) {
if (positional && glm::distance2(_listenerPosition.load(), position) > kMaxPositionalSoundDistance2) return nullptr;
auto sound = make_shared<SoundInstance>(stream, loop, getGain(type, gain), positional, move(position));
enqueue(sound);
return sound->handle();
}

View file

@ -27,7 +27,7 @@
namespace reone {
/**
* Utility class for caching objects. Requires a function which computes an object by key.
* Utility class for caching objects. Takes a function which computes an object by key.
*/
template <class K, class V>
class MemoryCache : boost::noncopyable {

View file

@ -63,12 +63,14 @@ bool CollisionDetector::rayTestRooms(const RaycastProperties &props, RaycastResu
shared_ptr<Walkmesh> walkmesh(room.second->walkmesh());
if (!walkmesh) continue;
if (walkmesh->raycast(props.origin, props.direction, props.flags & RaycastFlags::walkable, distance) &&
int material = 0;
if (walkmesh->raycast(props.origin, props.direction, props.flags & RaycastFlags::walkable, distance, material) &&
distance <= props.distance) {
result.room = room.second.get();
result.intersection = props.origin + distance * props.direction;
result.distance = distance;
result.material = material;
return true;
}
}
@ -111,7 +113,9 @@ bool CollisionDetector::rayTestObjects(const RaycastProperties &props, RaycastRe
} else {
// Test using a walkmesh
shared_ptr<Walkmesh> walkmesh(object->getWalkmesh());
if (walkmesh && walkmesh->raycast(objSpaceOrigin, objSpaceDir, props.flags & RaycastFlags::walkable, distance) &&
int material = 0;
if (walkmesh && walkmesh->raycast(objSpaceOrigin, objSpaceDir, props.flags & RaycastFlags::walkable, distance, material) &&
distance <= props.distance) {
collisions.push_back(make_pair(object, distance));

View file

@ -56,6 +56,7 @@ struct RaycastResult {
std::shared_ptr<SpatialObject> object;
glm::vec3 intersection { 0.0f };
float distance { 0.0f };
int material { -1 };
};
class CollisionDetector : boost::noncopyable {

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "footstepsounds.h"
#include <boost/format.hpp>
#include "../audio/files.h"
#include "../resource/resources.h"
using namespace std;
using namespace std::placeholders;
using namespace reone::audio;
using namespace reone::resource;
namespace reone {
namespace game {
FootstepSounds &FootstepSounds::instance() {
static FootstepSounds instance;
return instance;
}
FootstepSounds::FootstepSounds() : MemoryCache(bind(&FootstepSounds::doGet, this, _1)) {
}
shared_ptr<FootstepTypeSounds> FootstepSounds::doGet(uint32_t type) {
shared_ptr<FootstepTypeSounds> result;
shared_ptr<TwoDA> twoDa(Resources::instance().get2DA("footstepsounds"));
if (twoDa) {
result = make_shared<FootstepTypeSounds>();
map<string, vector<shared_ptr<AudioStream>> &> dict {
{ "dirt", result->dirt },
{ "grass", result->grass },
{ "stone", result->stone },
{ "wood", result->wood },
{ "water", result->water },
{ "carpet", result->carpet },
{ "metal", result->metal },
{ "leaves", result->leaves }
};
for (auto &pair : dict) {
for (int i = 0; i < 3; ++i) {
string key(str(boost::format("%s%d") % pair.first % i));
string resRef(twoDa->getString(static_cast<int>(type), key));
shared_ptr<AudioStream> audio(AudioFiles::instance().get(resRef));
pair.second.push_back(move(audio));
}
}
}
return move(result);
}
} // namespace game
} // namespace reone

53
src/game/footstepsounds.h Normal file
View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <vector>
#include "../audio/stream.h"
#include "../common/cache.h"
namespace reone {
namespace game {
struct FootstepTypeSounds {
std::vector<std::shared_ptr<audio::AudioStream>> dirt;
std::vector<std::shared_ptr<audio::AudioStream>> grass;
std::vector<std::shared_ptr<audio::AudioStream>> stone;
std::vector<std::shared_ptr<audio::AudioStream>> wood;
std::vector<std::shared_ptr<audio::AudioStream>> water;
std::vector<std::shared_ptr<audio::AudioStream>> carpet;
std::vector<std::shared_ptr<audio::AudioStream>> metal;
std::vector<std::shared_ptr<audio::AudioStream>> leaves;
};
class FootstepSounds : public MemoryCache<uint32_t, FootstepTypeSounds> {
public:
static FootstepSounds &instance();
private:
FootstepSounds();
std::shared_ptr<FootstepTypeSounds> doGet(uint32_t type);
};
} // namespace game
} // namespace reone

View file

@ -165,8 +165,9 @@ void Area::loadPTH() {
const Path::Point &point = points[i];
Room *room = nullptr;
float z = 0.0f;
int material = 0;
if (!getElevationAt(glm::vec2(point.x, point.y), room, z)) {
if (!getElevationAt(glm::vec2(point.x, point.y), room, z, material)) {
warn(boost::format("Area: point %d elevation not found") % i);
continue;
}
@ -273,8 +274,9 @@ void Area::add(const shared_ptr<SpatialObject> &object) {
void Area::determineObjectRoom(SpatialObject &object) {
glm::vec3 position(object.position());
Room *room = nullptr;
int material = 0;
if (getElevationAt(position, room, position.z)) {
if (getElevationAt(position, room, position.z, material)) {
object.setRoom(room);
}
}
@ -344,8 +346,9 @@ shared_ptr<SpatialObject> Area::getObjectByTag(const string &tag, int nth) const
void Area::landObject(SpatialObject &object) {
glm::vec3 position(object.position());
Room *room = nullptr;
int material = 0;
if (getElevationAt(position, room, position.z, true, &object)) {
if (getElevationAt(position, room, position.z, material, true, &object)) {
object.setPosition(position);
return;
}
@ -353,7 +356,7 @@ void Area::landObject(SpatialObject &object) {
float angle = i * glm::half_pi<float>();
position = object.position() + glm::vec3(glm::sin(angle), glm::cos(angle), 0.0f);
if (getElevationAt(position, room, position.z, true, &object)) {
if (getElevationAt(position, room, position.z, material, true, &object)) {
object.setPosition(position);
return;
}
@ -429,7 +432,7 @@ void Area::printDebugInfo(const SpatialObject &object) {
debug("Selected object: " + ss.str());
}
bool Area::getElevationAt(const glm::vec2 &position, Room *&room, float &z, bool creatures, const SpatialObject *except) const {
bool Area::getElevationAt(const glm::vec2 &position, Room *&room, float &z, int &material, bool creatures, const SpatialObject *except) const {
// Test AABB of alive creatures
if (creatures) {
RaycastProperties props;
@ -471,6 +474,7 @@ bool Area::getElevationAt(const glm::vec2 &position, Room *&room, float &z, bool
if (_collisionDetector.raycast(props, result)) {
room = result.room;
z = result.intersection.z;
material = result.material;
return true;
}
}
@ -539,12 +543,14 @@ bool Area::moveCreature(const shared_ptr<Creature> &creature, const glm::vec2 &d
bool Area::doMoveCreature(const shared_ptr<Creature> &creature, const glm::vec3 &dest) {
float z;
Room *room;
int material;
if (getElevationAt(dest, room, z)) {
if (getElevationAt(dest, room, z, material)) {
const Room *oldRoom = creature->room();
creature->setRoom(room);
creature->setPosition(glm::vec3(dest.x, dest.y, z));
creature->setWalkmeshMaterial(material);
if (creature == _game->party().getLeader()) {
onPartyLeaderMoved(room != oldRoom);

View file

@ -324,7 +324,7 @@ private:
*/
bool getCreatureObstacle(const Creature &creature, const glm::vec3 &dest) const;
bool getElevationAt(const glm::vec2 &position, Room *&room, float &z, bool creatures = false, const SpatialObject *except = nullptr) const;
bool getElevationAt(const glm::vec2 &position, Room *&room, float &z, int &material, bool creatures = false, const SpatialObject *except = nullptr) const;
// END Collision detection
};

View file

@ -24,6 +24,7 @@
#include "../../audio/player.h"
#include "../../common/log.h"
#include "../../common/random.h"
#include "../../common/streamutil.h"
#include "../../common/timer.h"
#include "../../render/model/models.h"
@ -35,7 +36,9 @@
#include "../action/attack.h"
#include "../animationutil.h"
#include "../footstepsounds.h"
#include "../portraits.h"
#include "../surfaces.h"
#include "objectfactory.h"
@ -83,8 +86,9 @@ void Creature::loadFromBlueprint(const string &resRef) {
void Creature::loadAppearance() {
shared_ptr<TwoDA> appearances(Resources::instance().get2DA("appearance"));
_modelType = parseModelType(appearances->getString(_appearance, "modeltype"));
_walkSpeed = appearances->getFloat(_appearance, "walkdist", 0.0f);
_runSpeed = appearances->getFloat(_appearance, "rundist", 0.0f);
_walkSpeed = appearances->getFloat(_appearance, "walkdist");
_runSpeed = appearances->getFloat(_appearance, "rundist");
_footstepType = appearances->getInt(_appearance, "footsteptype", -1);
if (_portraitId > 0) {
_portrait = Portraits::instance().getTextureByIndex(_portraitId);
@ -644,6 +648,40 @@ void Creature::setAppliedForce(glm::vec3 force) {
}
}
void Creature::onEventSignalled(const string &name) {
if (name == "snd_footstep" && _footstepType != -1 && _walkmeshMaterial != -1) {
shared_ptr<FootstepTypeSounds> sounds(FootstepSounds::instance().get(_footstepType));
if (sounds) {
const Surface &surface = Surfaces::instance().getSurface(_walkmeshMaterial);
vector<shared_ptr<AudioStream>> materialSounds;
if (surface.sound == "DT") {
materialSounds = sounds->dirt;
} else if (surface.sound == "GR") {
materialSounds = sounds->grass;
} else if (surface.sound == "ST") {
materialSounds = sounds->stone;
} else if (surface.sound == "WD") {
materialSounds = sounds->wood;
} else if (surface.sound == "WT") {
materialSounds = sounds->water;
} else if (surface.sound == "CP") {
materialSounds = sounds->carpet;
} else if (surface.sound == "MT") {
materialSounds = sounds->metal;
} else if (surface.sound == "LV") {
materialSounds = sounds->leaves;
}
int index = random(0, 3);
if (index < static_cast<int>(materialSounds.size())) {
shared_ptr<AudioStream> sound(materialSounds[index]);
if (sound) {
AudioPlayer::instance().play(sound, AudioType::Sound, false, 1.0f, true, _position);
}
}
}
}
}
} // namespace game
} // namespace reone

View file

@ -26,6 +26,7 @@
#include "../../resource/format/2dareader.h"
#include "../../resource/format/gffreader.h"
#include "../../resource/types.h"
#include "../../scene/animation/eventlistener.h"
#include "../../script/types.h"
#include "../d20/attributes.h"
@ -41,7 +42,7 @@ namespace game {
constexpr float kDefaultAttackRange = 2.0f;
class Creature : public SpatialObject {
class Creature : public SpatialObject, public scene::IAnimationEventListener {
public:
enum class ModelType {
Creature,
@ -124,6 +125,7 @@ public:
RacialType racialType() const { return _race; }
Subrace subrace() const { return _subrace; }
NPCAIStyle aiStyle() const { return _aiStyle; }
int walkmeshMaterial() const { return _walkmeshMaterial; }
void setGender(Gender gender) { _gender = gender; }
void setAppearance(int appearance) { _appearance = appearance; }
@ -133,6 +135,7 @@ public:
void setImmortal(bool immortal) { _immortal = immortal; }
void setXP(int xp) { _xp = xp; }
void setAIStyle(NPCAIStyle style) { _aiStyle = style; }
void setWalkmeshMaterial(int material) { _walkmeshMaterial = material; }
// Animation
@ -213,6 +216,12 @@ public:
// END Scripts
// IAnimationEventListener
void onEventSignalled(const std::string &name);
// END IAnimationEventListener
private:
Gender _gender { Gender::Male };
int _appearance { 0 };
@ -255,6 +264,8 @@ private:
int _lawfulChaotic { 0 };
int _challengeRating { 0 };
bool _disarmable { false };
uint32_t _footstepType { 0 };
int _walkmeshMaterial { -1 };
// Animation

View file

@ -50,7 +50,7 @@ shared_ptr<ModelSceneNode> Creature::buildModel() {
shared_ptr<Model> model(Models::instance().get(modelName));
if (!model) return nullptr;
auto modelSceneNode = make_unique<ModelSceneNode>(ModelUsage::Creature, model, _sceneGraph);
auto modelSceneNode = make_unique<ModelSceneNode>(ModelUsage::Creature, model, _sceneGraph, set<string>(), this);
// Body texture

View file

@ -27,7 +27,7 @@ struct Surface {
std::string label;
bool walkable { false };
bool grass { false };
std::string soundResRef;
std::string sound;
};
} // namespace game

View file

@ -43,7 +43,7 @@ void Surfaces::init() {
surface.label = surfacemat->getString(row, "label");
surface.walkable = surfacemat->getBool(row, "walk");
surface.grass = surfacemat->getBool(row, "grass");
surface.soundResRef = boost::to_lower_copy(surfacemat->getString(row, "sound"));
surface.sound = surfacemat->getString(row, "sound");
_surfaces.push_back(move(surface));
}
}
@ -57,7 +57,7 @@ bool Surfaces::isWalkable(int index) const {
const Surface &Surfaces::getSurface(int index) const {
if (index < 0 || index >= static_cast<int>(_surfaces.size())) {
throw out_of_range("index is out of range");
throw out_of_range("index is out of range: " + to_string(index));
}
return _surfaces[index];
}

View file

@ -41,7 +41,7 @@ void Walkmesh::computeAABB() {
}
}
bool Walkmesh::raycast(const glm::vec3 &origin, const glm::vec3 &dir, bool walkable, float &distance) const {
bool Walkmesh::raycast(const glm::vec3 &origin, const glm::vec3 &dir, bool walkable, float &distance, int &material) const {
float minDistance = FLT_MAX;
const vector<Face> &faces = walkable ? _walkableFaces : _nonWalkableFaces;
@ -55,6 +55,7 @@ bool Walkmesh::raycast(const glm::vec3 &origin, const glm::vec3 &dir, bool walka
if (glm::intersectRayTriangle(origin, dir, p0, p1, p2, baryPosition, localDistance) && localDistance >= 0.0f && localDistance < minDistance) {
minDistance = localDistance;
material = static_cast<int>(face.material);
}
}

View file

@ -42,7 +42,7 @@ public:
float area { 0.0f };
};
bool raycast(const glm::vec3 &origin, const glm::vec3 &dir, bool walkable, float &distance) const;
bool raycast(const glm::vec3 &origin, const glm::vec3 &dir, bool walkable, float &distance, int &material) const;
const std::vector<Face> &grassFaces() const { return _grassFaces; }
const AABB &aabb() const { return _aabb; }

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
namespace reone {
namespace scene {
class IAnimationEventListener {
public:
virtual ~IAnimationEventListener() = default;
virtual void onEventSignalled(const std::string &name) = 0;
};
} // namespace scene
} // namespace reone

View file

@ -47,8 +47,15 @@ const float kMinDirectionalLightRadius = 1000.0f;
static bool g_debugAABB = false;
ModelSceneNode::ModelSceneNode(ModelUsage usage, const shared_ptr<Model> &model, SceneGraph *sceneGraph, set<string> ignoreNodes) :
ModelSceneNode::ModelSceneNode(
ModelUsage usage,
const shared_ptr<Model> &model,
SceneGraph *sceneGraph,
set<string> ignoreNodes,
IAnimationEventListener *animEventListener
) :
SceneNode(SceneNodeType::Model, sceneGraph),
_animEventListener(animEventListener),
_usage(usage),
_model(model),
_animator(this, ignoreNodes) {
@ -370,6 +377,8 @@ void ModelSceneNode::signalEvent(const string &name) {
for (auto &emitter : _emitters) {
emitter->detonate();
}
} else if (_animEventListener) {
_animEventListener->onEventSignalled(name);
}
}

View file

@ -24,6 +24,7 @@
#include "../../render/shaders.h"
#include "../../render/walkmesh/walkmesh.h"
#include "../animation/eventlistener.h"
#include "../animation/scenenodeanimator.h"
#include "../types.h"
@ -43,7 +44,8 @@ public:
ModelUsage usage,
const std::shared_ptr<render::Model> &model,
SceneGraph *sceneGraph,
std::set<std::string> ignoreNodes = std::set<std::string>());
std::set<std::string> ignoreNodes = std::set<std::string>(),
IAnimationEventListener *animEventListener = nullptr);
void update(float dt) override;
void draw() override;
@ -97,6 +99,8 @@ public:
// END Dynamic lighting
private:
IAnimationEventListener *_animEventListener;
ModelUsage _usage;
std::shared_ptr<render::Model> _model;
std::shared_ptr<render::Walkmesh> _walkmesh;