diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index cbb5605..15c0651 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -22,6 +22,10 @@ set(GAME_SRC util.cpp game.cpp + # TODO: make optional! + sound/sound.cpp + sound/sound_effects.cpp + state/object.cpp state/explosion.cpp state/trace.cpp @@ -43,4 +47,4 @@ include_directories(${assimp_INCLUDE_DIRS}) add_executable(game ${GAME_SRC}) setup_target(game) -target_link_libraries(game X11 epoxy pthread ${assimp_LIBRARIES} assimp) +target_link_libraries(game X11 epoxy pthread ${assimp_LIBRARIES} assimp -lportaudio -lsndfile) diff --git a/game/main.cpp b/game/main.cpp index 6460cab..1e4afd1 100644 --- a/game/main.cpp +++ b/game/main.cpp @@ -10,6 +10,9 @@ #include "network/server.hpp" #include "options.hpp" +#include "sound/sound_effects.hpp" +#include "state/state_update_event.hpp" + #include uint64_t optionsFlags; @@ -19,6 +22,7 @@ using asio::ip::tcp; int main(int argc, char *argv[]) { bool devMode = false; + bool soundEnabled = false; char port[]="3490"; static struct option long_options[] = @@ -33,6 +37,7 @@ int main(int argc, char *argv[]) // {"delete", required_argument, 0, 'd'}, {"autorun", required_argument, 0, 'a'}, {"port", required_argument, 0, 'p'}, + {"sound", no_argument, 0, 's'}, {"fps", no_argument, 0, 'f'}, {"dev", no_argument, 0, 'd'}, {0, 0, 0, 0} @@ -41,7 +46,7 @@ int main(int argc, char *argv[]) int option_index = 0; while(1){ - char c = getopt_long (argc, argv, "p:fad", + char c = getopt_long (argc, argv, "p:fads", long_options, &option_index); if (c == -1) break; @@ -51,6 +56,7 @@ int main(int argc, char *argv[]) case 'f': SET_FLAG(SHOW_FPS,true); break; + case 'd': std::cout<<"enabling developer mode" << std::endl; devMode = true; @@ -60,6 +66,10 @@ int main(int argc, char *argv[]) strcpy(port,optarg); break; + case 's': + soundEnabled = true; + break; + case 'a': SET_FLAG(TEST_AUTORUN, true); break; @@ -75,6 +85,13 @@ int main(int argc, char *argv[]) srand(time(NULL)); + //sound::SoundEffects *sounds = nullptr; + if (soundEnabled) { + //if (sound::initSound()) { + //sounds = new sound::SoundEffects(); + //} + } + Game game; game.state()->setDeveloperMode(devMode); @@ -89,6 +106,10 @@ int main(int argc, char *argv[]) while(window.running()){ window.poll(); io_service.poll(); + + //if (sounds != nullptr) { + // sounds.advance(game->state()); + //} } return 0; diff --git a/game/network/session.hpp b/game/network/session.hpp index 730dc93..91f5663 100644 --- a/game/network/session.hpp +++ b/game/network/session.hpp @@ -33,5 +33,5 @@ private: char m_rcv_data[max_length]; game::State* m_state; bool m_started = false; - int m_pid; + size_t m_pid; }; diff --git a/game/sound/sound.cpp b/game/sound/sound.cpp new file mode 100644 index 0000000..3c9f433 --- /dev/null +++ b/game/sound/sound.cpp @@ -0,0 +1,968 @@ +#include "sound.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include "util.hpp" + +// for dumping sound +#include + +namespace sound { + + typedef struct { + PaStream *stream; + + unsigned int framesPerBuffer; + unsigned int sampleRate; + + unsigned int sound_uid_counter; + + // constant + unsigned int maxNumSounds; + + // num active sonuds + unsigned int highestSoundIndex; + + // pointer to array of sound handles that has this number of active sounds + SoundHandle **sounds; + + bool dumping; + const char *dump_filename; + SNDFILE *outfile; + SF_INFO sfinfo; + + bool init; + } SoundContext; + + // singleton so no arguments must be supplied for play() functions etc. + static SoundContext context; + + static double startup_time = -1.0; + static double global_time = -1.0;; + + static double timestamp_double(void) + { + struct timeval now; + gettimeofday(&now, NULL); + + //printf("%d %d\n", (int) now.tv_sec, (int) now.tv_usec); + + double t = now.tv_sec; + t += now.tv_usec / 1000000.0f; + //printf("%f\n", t); + return t; + } + + const char *stringFromEnvelope(enum EnvelopeState envelope) + { + switch(envelope) { + case New: return "New"; + case Wait: return "Wait"; + case Rise: return "Rise"; + case BeginOvershoot: return "BeginOvershoot"; + case EndOvershoot: return "EndOvershoot"; + case Decay: return "Decay"; + case Hold: return "Hold"; + case Done: return "Done"; + default: assert(false); return ""; + } + } + + static void checkEnvelopeState(SoundHandle *handle, enum EnvelopeState next) + { + if (handle == NULL) { + return; + } + + if (handle->envelope != next) { +#if 1 + printf("[sound] global time %4.3f #%d (%s) t=%f %5s -> %5s\n", + global_time, + handle->uid, + handle->name, + handle->time, + stringFromEnvelope(handle->envelope), + stringFromEnvelope(next)); +#endif + + handle->envelope = next; + } + } + + void stopDumpingWav(void) + { + if (!context.init || !context.dumping) { + return; + } + + int err = sf_close(context.outfile); + context.outfile = NULL; + context.dumping = false; + + if (err < 0) { + printf("warning: stop dumping wav returned error: %d\n", err); + } else { + printf("sucessfully stopped dumping to file %s\n", context.dump_filename); + } + } + + static bool dumpSamples(float *samples, size_t numSamples) + { + // TODO: dumping in mono? + + if (!context.init || !context.dumping) { + printf("warning: can't dump samples: context not initialized or not currently dumping!\n"); + return false; + } + + sf_writef_float(context.outfile, samples, numSamples); + sf_write_sync(context.outfile); // force writing + + return true; + } + + bool startDumpingWav(const char *filename) + { + if (!context.init) { + return false; + } + + if (context.dumping) { + printf("warning: already dumping to %s!\n", context.dump_filename); + return false; + } + + context.sfinfo.channels = 2; + context.sfinfo.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT; + context.sfinfo.samplerate = context.sampleRate; + + context.outfile = sf_open(filename, SFM_WRITE, &context.sfinfo); + + if (context.outfile == NULL) { + printf("Unable to dump to output file %s.\n", filename); + sf_perror(NULL); + return false; + }; + + //printf("start dumping wav to file %s\n", filename); + context.dump_filename = filename; + context.dumping = true; + return true; + } + + static float amplitudeForTime(SoundHandle *handle) + { + //float amplitude = handle->amplitude; + float amplitude = 1.0; + + float t = handle->time; + if (t < handle->_riseTime) { + // linear increase + float a = t / handle->_riseTime; + + if (handle->_relativeOvershootTime == 0.0) { + checkEnvelopeState(handle, Rise); + + } else { + // + // |--overshoot-up-| |-overshoot-down-| + // |-------------overshoot------------| + // |-----------------rise-time -----------------| + + // make it raise to full level earlier + const float overshoot = 2.0*handle->_relativeOvershootTime; + + a *= 1.0+overshoot; + + // compensate the overshoot during last r/2 time to normal level + // (1.0) by inversing slope + if (a < 1.0) { + checkEnvelopeState(handle, Rise); + + } else if (a < 1.0 + (overshoot/2.0)) { + checkEnvelopeState(handle, BeginOvershoot); + + } else { + a = 2.0 + overshoot - a; + checkEnvelopeState(handle, EndOvershoot); + + } + } + + amplitude *= a; + //printf("amplitude rising: %f\n", a); + + } else { + t -= handle->_riseTime; + if (t < handle->_holdTime) { + // stays constant during this time + checkEnvelopeState(handle, Hold); + + } else { + t -= handle->_holdTime; + if (t < handle->_decayTime) { + // linear decrease + float a = 1.0 - (t / handle->_decayTime); + //printf("amplitude decaying : %f\n", a); + amplitude *= a; + checkEnvelopeState(handle, Decay); + + } else { + bool switchToDone = false; + t -= handle->_decayTime; + if (t < handle->_waitTime) { + // still waiting + checkEnvelopeState(handle, Wait); + + } else if (handle->looping && (handle->_loopCount == -1 || handle->_loopCount > 0)) { + // waited long enough to restart + // TODO: dont waste time! does stuff stay synchronized + // over a long period of time? + //handle->time = 0.0; + handle->time -= handle->_riseTime + handle->_waitTime + handle->_holdTime + handle->_decayTime; + + if (handle->_loopCount != -1) { + handle->_loopCount--; + //printf("decrement loopCount to %d\n", handle->_loopCount); + if (handle->_loopCount == 0) { + switchToDone = true; + } + } + } else { + switchToDone = true; + } + + if (switchToDone) { + // zero + checkEnvelopeState(handle, Done); + handle->_done = true; + return 0.0; + } + amplitude = 0.0; + } + } + } + + //int phase = handle->envelope; + //printf("%f, %d, %f\n", handle->time, phase, amplitude); + + if (handle->minAmplitude != 0.0) { + amplitude = handle->minAmplitude + amplitude/(1.0-handle->minAmplitude); + } + + amplitude *= handle->amplitude; + + return amplitude; + } + + // TODO: support stereo fully + + // args: + // n: n-th last history value + static float historyValue(SoundHandle *handle, size_t n) + { + if (handle == NULL) { + return 0.0; + } + if (handle->history == NULL) { + return 0.0; + } + if (n >= handle->numHistorySamples) { + // TODO clear history when looping? + // or add zero samples? + printf("warning: try to access more history values than saved.\n"); + return 0.0; + } + + return handle->history[2 * ((handle->lastHistorySample+n) % handle->numHistorySamples)]; + } + + void saveHistoryValue(SoundHandle *handle, float v) + { + if (handle == NULL || handle->history == NULL) { + return; + } + + handle->history[2*handle->lastHistorySample] = v; + handle->lastHistorySample = (handle->lastHistorySample+1) % handle->numHistorySamples; + } + + float sumHistory(SoundHandle *handle, float initialWeight, float v) + { + float result = initialWeight * v; + float totalWeight = initialWeight; + + size_t i; + for (i=0; inumHistorySamples; i++) { + float weight = 1.0f / (float) (1+i); + //float weight = 1.0f; + result += weight * historyValue(handle, i); + totalWeight += weight; + } + + result /= totalWeight; + return result; + } + + // return false if playing done. + static bool advanceSound(SoundHandle *handle, float *out, unsigned int framesPerBuffer) + { + assert(handle != NULL); + assert(framesPerBuffer > 0); + assert(out != NULL); + + if (handle == NULL || framesPerBuffer == 0 || out == NULL) { + return false; + } + + // TODO: add length to sounds + + // TODO: find out the period, write just one period and then copy the + // values. + //float dt = handle->freq / context.sampleRate; + + // TODO: divide into three parts for envelops + // because they are linear in the phases so interpolating between that + // should be made easier + + // advance parameters for testing + switch(handle->_type) { + //float div = 10.0; + //float t = (((int)(100*handle->time/div)) % 101) / 100.0; + //handle->leakage = 0.5+0.5*sin(2.0*M_PI*t); + default: + break; + } + + const float dt = 1.0 / context.sampleRate; + const float ds = 2.0 * M_PI * handle->freq * dt; + + // XXX too long range when becomes done + unsigned int i; + for (i=0; itime += dt; + + // TODO: save this state too? + if (handle->time < 0.0) { + checkEnvelopeState(handle, Wait); + // this sound can not yet be heard + continue; + } + + // TODO: add phase as argument. + + float v; + switch (handle->_type) { + case SoundFrequency: + handle->state += ds; + v = sin(handle->state); + break; + + case SoundWhiteNoise: + v = util::randf_m1_1(); + break; + + case SoundPinkNoise: + //v = (historyValue(handle, 0) + Randf_m1_1()) / 2.0f; + //saveHistoryValue(handle, v); + v = (handle->state + util::randf_m1_1()) / 2.0f; + handle->state = v; + break; + + case SoundBrownianNoise: + // this is created by integrating white noise. + + { + float white = util::randf_m1_1(); + float p = handle->leakage * handle->state; + float s = handle->scaling * white; + float z = p + s; + if (handle->skipDirectionIfClamping && fabsf(z) >= 1.0f) { + z = p - s; + } + handle->state = z; + v = z; + } + //v = sumHistory(handle, handle->freq, randf_m1_1()); + //v = sumHistory(handle, 0.1, randf_m1_1()); + //v = 0.2*historyValue(handle, 1) + 0.3*historyValue(handle, 0) + 0.5*randf_m1_1(); + //v = (historyValue(handle, 1) + historyValue(handle, 0) + randf_m1_1()) / 3.0; + //saveHistoryValue(handle, v); + +#if 0 + { + float a = 0.8; + v = a*handle->state + (1.0-a)*randf_m1_1(); + handle->state = v; + } +#endif + break; + + default: + printf("warning: unimplemented sound type for #%d: %d\n", handle->uid, handle->_type); + continue; + } + + if (handle->_useEnvelope) { + v *= amplitudeForTime(handle); + if (handle->_done) { + return false; + } + } else { + v *= handle->amplitude; + } + + // add values from this sound te to current sound level + *out++ += v; + *out++ += v; + } + + return true; + } + + /* This routine will be called by the PortAudio engine when audio is needed. + It may called at interrupt level on some machines so don't do anything + that could mess up the system like calling malloc() or free(). + */ + static int sound_callback(const void *inputBuffer, + void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData) + { + (void) inputBuffer; + (void) timeInfo; + (void) statusFlags; + (void) userData; + + // update time + double now = timestamp_double(); + + // should not happen. just in case. + global_time = now - startup_time; + + //printf("global time raw: %f\n", global_time); + if (global_time < 0.0) { + global_time = 0.0; + } + + // we have 32 bit float format so cast here + float *out = (float *) outputBuffer; + + // set all to zero + unsigned int i; + for (i=0; i_done) { +#if 0 + printf("play sound 0x%x with loop=%d, sample=%d, freq=%f amplitude=%f\n", + (void*) handle, + handle->looping, + (unsigned int) handle->_currentSample, handle->freq, handle->amplitude); +#endif + + if (!advanceSound(handle, out, framesPerBuffer)) { + handle->_done = true; + } + } + + // TODO: when we should not free in the loop, where else? + if (handle->_done) { + //printf("[sound] %f freeing sound #%d handle 0x%p\n", global_time, handle->uid, handle); + context.sounds[i] = NULL; + if (!handle->keep_when_done) { + deleteSound(handle); + } + } + } + + // make sure all -1.0 >= x <= 1 +#if 0 + out = outputBuffer; + for (i=0; i<2*framesPerBuffer; i++) { + if (*out < -1.0) { *out = -1.0; } + else if (*out > 1.0) { *out = 1.0; } + } +#endif + + if (context.dumping) { + dumpSamples((float *) outputBuffer, framesPerBuffer); + } + + // TODO: check! +#if 0 + // apply post-processing + out = outputBuffer; + // advance all sounds + for (i=0; iname); + } +#endif + } + + /* Open an audio I/O stream. */ + err = Pa_OpenDefaultStream( + &context.stream, + 0, /* no input channels */ + 2, /* stereo output */ + paFloat32, /* 32 bit floating point output */ + context.sampleRate, /* sample rate */ + context.framesPerBuffer, /* frames per buffer */ + sound_callback, /* this is your callback function */ + NULL); /* callback function pointer */ + + if (err != paNoError) { + printf("error: opening default stream: %s\n", Pa_GetErrorText(err)); + return false; + } + + err = Pa_StartStream(context.stream); + if (err != paNoError) { + printf("error: starting default stream failed: %s\n", Pa_GetErrorText(err)); + return false; + } + + // clear that + //sound_context.num_active_handles = 0; + //sound_context.num_cached = 0; + + context.init = true; + return true; + } + + static void allocateHistory(SoundHandle *handle, size_t numHistorySamples) + { + handle->lastHistorySample = 0; + + if (numHistorySamples == 0) { + handle->numHistorySamples = 0; + handle->history = NULL; + return; + } + + handle->numHistorySamples = numHistorySamples; + handle->history = (float *) calloc(sizeof(float)*2, handle->numHistorySamples); + + // clear all + size_t i; + for (i=0; i<2*handle->numHistorySamples; i++) { + handle->history[i] = 0.0f; + } + } + + SoundHandle *playFrequency(float freq, float amplitude) + { + SoundHandle *handle = playSound(SoundFrequency, amplitude); + if (handle != NULL) { + handle->freq = freq; + //handle->looping = true; + } + return handle; + } + + SoundHandle *playSound(enum Sound type, float amplitude) + { + if (!context.init) { + //printf("warning: called playSound() with no initialized sound system!\n"); + return NULL; + } + + size_t nextFreeIndex = 0; + while(nextFreeIndex < context.highestSoundIndex) { + if (context.sounds[nextFreeIndex] == NULL) { + // got an empyt sound to use + break; + } + nextFreeIndex++; + } + + if (nextFreeIndex >= context.maxNumSounds) { + printf("error: can't create more sounds, got maximum of: %d\n", context.maxNumSounds); + return NULL; + } + + SoundHandle *handle = (SoundHandle *) calloc(1, sizeof(SoundHandle)); + handle->envelope = New; + handle->amplitude = amplitude; + handle->minAmplitude = 0.0; + handle->freq = 1000; + handle->looping = false; + handle->_loopCount = 0; + handle->_type = type; + handle->time = 0.0; + handle->keep_when_done = false; + handle->state = 0.0; + handle->leakage = 0.0; + handle->scaling = 0.0; + handle->_done = false; + handle->skipDirectionIfClamping = false; + + handle->_useEnvelope = false; + handle->_riseTime = 0.0; + handle->_relativeOvershootTime = 0.0; + handle->_decayTime = 0.0; + handle->_holdTime = 0.0; + handle->_waitTime = 0.0; + handle->decayType = SoundDecayLinear; + handle->riseType = SoundDecayLinear; + handle->name = ""; + + // filters may to access values from their history + int numHistoryValues = 0; + + switch(type) { + // pink/white/brown noise need to save only one sample, state is enough + // for that. + case SoundBrownianNoise: + // something like lowpass filter constant. + // lower value == lower freq. noise + // values that audacity uses + //handle->leakage = 0.997; + //handle->scaling = 0.05; + + handle->leakage = (context.sampleRate - 144.0) / context.sampleRate; + handle->scaling = 9.0 / sqrt(context.sampleRate); + //printf("computed leakage and scaling: %f %F\n", handle->leakage, handle->scaling); + + //handle->leakage = 0.7; + //handle->scaling = 0.2; + handle->skipDirectionIfClamping = true; + break; + + case SoundPinkNoise: + break; + + case SoundWhiteNoise: + break; + + default: + break; + } + + allocateHistory(handle, numHistoryValues); + + handle->uid = context.sound_uid_counter++; + + context.sounds[nextFreeIndex] = handle; + + if (nextFreeIndex == context.highestSoundIndex) { + context.highestSoundIndex++; + } + + return handle; + } + + bool stopSound(SoundHandle *handle) + { + if (!context.init) { + printf("warning: called stopSound() with no initialized sound system!\n"); + return false; + } + + if (handle == NULL) { + printf("warning: stopSound() handle is NULL\n"); + return false; + } + + if (handle->_done) { + printf("warning: stopSound() sound is already stopped\n"); + return false; + } + + // TODO: kill with variable + handle->_done = true; + + return true; + } + + void stopAllSounds(void) + { + if (!context.init) { + printf("warning: called stopAllSounds() with no initialized sound system!\n"); + } + + unsigned int i; + for (i=0; i_done) { + stopSound(context.sounds[i]); + } + } + } + + void computeEnvelopeUse(SoundHandle *handle) + { + if (handle == NULL) { + return; + } + + // wait time has no influence (yet) + handle->_useEnvelope = (handle->_riseTime != 0.0 || handle->_holdTime != 0.0 || handle->_decayTime != 0.0); + } + + bool configureEnvelope(SoundHandle *handle, float rise, float hold, float decay) + { + if (handle == NULL) { + return false; + } + + // TODO: check inf/nan too + if (rise < 0.0) { + return false; + } + if (hold < 0.0) { + return false; + } + if (decay < 0.0) { + return false; + } + + handle->_riseTime = rise; + handle->_holdTime = hold; + handle->_decayTime = decay; + + // set depending on flags + computeEnvelopeUse(handle); + + return true; + } + + bool configureEnvelopeLooping(SoundHandle *handle, float rise, float hold, float decay, float wait) + { + if (wait < 0.0) { + return false; + } + + if (!configureEnvelope(handle, rise, hold, decay)) { + return false; + } + + handle->looping = true; + handle->_loopCount = -1; + handle->_waitTime = wait; + + return true; + } + + float getRiseDuration(SoundHandle *handle) + { + if (handle == NULL) { + return -1.0; + } + + return handle->_riseTime; + } + + float getHoldDuration(SoundHandle *handle) + { + if (handle == NULL) { + return -1.0; + } + + return handle->_holdTime; + } + + float getDecayDuration(SoundHandle *handle) + { + if (handle == NULL) { + return -1.0; + } + + return handle->_decayTime; + } + + float getwaitTime(SoundHandle *handle) + { + if (handle == NULL) { + return -1.0; + } + + return handle->_waitTime; + } + + void teardownSound(void) + { + if (!context.init) { + printf("[sonud] can't teardown sound when not initialized!\n"); + return; + } + + PaError err = Pa_CloseStream(context.stream); + if (err != paNoError) { + printf("[sound] portaudio failed to close stream: %d\n", err); + } + + err = Pa_Terminate(); + if (err != paNoError) { + printf("[sound] portaudio failed to terminate: %d\n", err); + } + + if (context.dumping) { + stopDumpingWav(); + } + + context.init = false; + } + + bool configureOvershoot(SoundHandle *handle, float relativeOvershootTime) + { + if (handle == NULL) { + return false; + } + if (relativeOvershootTime < 0.0) { + return false; + } + + handle->_relativeOvershootTime = relativeOvershootTime; + + return true; + } + + void setLoopCount(SoundHandle *handle, int numRepetitions) + { + if (numRepetitions < -1) { + numRepetitions = -1; + } + + handle->_loopCount = numRepetitions; + + if (numRepetitions == 0) { + handle->looping = false; + } else if (numRepetitions == -1) { + handle->looping = true; + } else { + handle->looping = true; + } + } + + int numActiveSounds(void) + { + int num = 0; + size_t i; + for (i=0; i_done = true; + + // clear from that + size_t i; + for (i=0; ihistory != NULL) { + free(handle->history); + } + + free(handle); + } + + bool configureEnvelopeWait(SoundHandle *handle, float rise, float hold, float decay, float wait) + { + if (configureEnvelopeLooping(handle, rise, hold, decay, wait)) { + handle->looping = false; + handle->_loopCount = -1; + return true; + + } else { + return false; + } + } +} diff --git a/game/sound/sound.hpp b/game/sound/sound.hpp new file mode 100644 index 0000000..ef65c07 --- /dev/null +++ b/game/sound/sound.hpp @@ -0,0 +1,185 @@ +#ifndef _SOUND_HPP_ +#define _SOUND_HPP_ + +#include +#include + +namespace sound { + + // type of sound. + // the envelope settings modify each sound. + enum Sound { + SoundFrequency=0, // one frequency + SoundWhiteNoise=1, // all frequencies are equal + SoundBrownianNoise=2, // 1/f^2 noise + SoundPinkNoise=3, // 1/f noise + }; + + enum EnvelopeState { + New=0, // newly spawned + Wait, // ready but not yet started (waiting before starting or before repeating) + Rise, // increasing + BeginOvershoot, + EndOvershoot, + Hold, // staying at normal level + Decay, // deacying + Done, // done + }; + + enum SoundDecayType { + SoundDecayLinear = 0, + SoundDecayExp = 1 + }; + + typedef struct { + // just for debugging/logging. don't rely on use + const char *name; + + // unique ID for accessing them via lookups etc. if sound is dead, this will be never reused + int uid; + + // if true, don't deallocate once this is done. + bool keep_when_done; + + // these are public and can be changed by the user. + enum EnvelopeState envelope; + + /****************************************************************/ + /* Special Parameters for specific types */ + /****************************************************************/ + + /******************** SoundFrequency ****************************/ + + float freq; // base frequency for type: SoundFrequency + + /******************** White / Brownian / Pink Noise ****************/ + float leakage; // influence of previous state on current value (0..1) + float scaling; // influence of white noise on next value + + // If true and the value goes outside (-1..1), do the step in the other + // direction to stay inside the valid range. + bool skipDirectionIfClamping; + + /******************** SoundFrequency: ***************************/ + + /****************************************************************/ + /* Common sound state / parameters */ + /****************************************************************/ + // progress of the sound which is used for the envelope but can be used for + // other things too + float time; + + // scalar state that can be used arbitrarily to derive the waveform from it + float state; + + // time when the sound is done + // if != 0.0, the sound changes with that + // TODO: make attack gain decay stuff as envelope + // TODO: add amplitude noise + float _riseTime; + float _relativeOvershootTime; // percentage of end of rise time that is used for overshoot (max amplitude at t_overshoot/2) + float _holdTime; + float _decayTime; + float _waitTime; + + // waveform of type of rise. default: linear + enum SoundDecayType riseType; + + // waveform of type of decay. default: linear + enum SoundDecayType decayType; + + // if true, play infinitely + bool looping; + + // base amplitude of the sound + float amplitude; + + // amplitude when signal is at minimum (before rise, after release and in wait time) + float minAmplitude; + + // these are al private! + // never acces them + enum Sound _type; // which type of sound + + int _loopCount; + + // if true, will get removed + bool _done; + bool _useEnvelope; + + float *history; // if != 0, history is a pointer to 2*numHistorySamples samples. + size_t numHistorySamples; // if != 0, save history for this sound. + size_t lastHistorySample; // index/2 of last history sample. used + } SoundHandle; + + // initialize sound subsystem. return true if working, false on errors. + bool initSound(void); + + // start playing a sound. + // args: + // type - type of sound. + // amplitude : 0..1 + // handle: if != NULL will point to the internal sound handle to change the + // manipulate playing of the sound. + // TODO: add sound from file to play too + SoundHandle *playSound(enum Sound type, float amplitude); + + // start playing a looping frequency. + // args: + // freq: frequency in Hz + SoundHandle *playFrequency(float freq, float amplitude); + + // set them. + // args: + // time of phases + bool configureEnvelope(SoundHandle *handle, float rise, float hold, float decay); + + bool configureOvershoot(SoundHandle *handle, float relativeOvershootTime); + + // same as above but set to looping and set wait time between intervals + bool configureEnvelopeWait(SoundHandle *handle, float rise, float hold, float decay, float wait); + + // without looping but with wait + bool configureEnvelopeLooping(SoundHandle *handle, float rise, float hold, float decay, float wait); + + // getter for attributes. if 0.0, unused. + float getRiseDuration(SoundHandle *handle); + float getHoldDuration(SoundHandle *handle); + float getDecayDuration(SoundHandle *handle); + + float getWaitTime(SoundHandle *handle); + void configureWaitTime(SoundHandle *handle, float waitTime); + + // Set looping behaviour of the sound. + // args: + // / > 0, sound is looping for this number of repetitions + // numRepetitions { == 0, sound is stopped when it is over + // \ == -1, loop the sound forever + // + void setLoopCount(SoundHandle *handle, int numRepetitions); + + // stop playing a sound. the handle is invalid now. + bool stopSound(SoundHandle *handle); + + // stop and remove all playing sounds. all handle become invalid after calling + // this. + void stopAllSounds(void); + + void teardownSound(void); + + // for debugging + bool startDumpingWav(const char *filename); + + // is deletede when 'keep' not set after stopping + void stopDumpingWav(void); + + int numActiveSounds(void); + + void deleteSound(SoundHandle *handle); + + //SoundHandle *playFreqEnvelope(float freq, float rise, float hold, float decay); + + // TODO: add fade out/fade in overlay stuff for stopping sounds smoothly +} + +#endif diff --git a/game/sound/sound_effects.cpp b/game/sound/sound_effects.cpp new file mode 100644 index 0000000..22ebb4e --- /dev/null +++ b/game/sound/sound_effects.cpp @@ -0,0 +1,77 @@ +#include "sound_effects.hpp" + +#include "sound.hpp" + +namespace sound { +#if 0 + class MissileFlySoundEffect : public SoundEffect { + MissileFlySoundEffect(const game::Missile *missile) + : m_missileId(missile->id) + , m_handle(sound::playSound(sound::playFrequency(440, 0.3)) + { + } + + ~MissileFlySoundEffect() + { + delete(m_handle); + } + + bool advance(float dt) + { + for (const game::StateUpdateEvent *event : updates) { + handleUpdateEvent(event); + } + + // TODO: add event if sth. comes into life or dies in a state + // cycle.. delete objects that died last cycle in the current cycle + // so pointers don't get invalid. + + bool keep = false; + for (const game::Missile *missile : state->missiles) { + if (missile->id == m_missileId) { + keep = true; + break; + } + } + + if (!keep) { + sound::stopSound(m_handle); + sound::deleteSound(m_handle); + return false; + } + + return true; + } + + private: + SoundHandle *m_handle; + size_t m_missileId; + }; + + void SoundEffects::advance(float dt, const std::list &updates) + { + //StateUpdateEvent::LifeCycle::Create, + //StateUpdateEvent::Type::Explosion + + std::vector rm; + + for (SoundEffect *effect : m_effects) { + bool keep = effect->advance(dt, m_gameState); + if (!keep) { + rm.push_back(effect); + } + } + + for (SoundEffect *effect : rm) { + m_effects.remove(rm); + delete(rm); + } + + // spawn new sounds + //for (const game::Missile *missile : state->missiles) { + // for (SoundEffect + // if (missile-> + //} + } +#endif +} diff --git a/game/sound/sound_effects.hpp b/game/sound/sound_effects.hpp new file mode 100644 index 0000000..984703a --- /dev/null +++ b/game/sound/sound_effects.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "state/state.hpp" + +namespace sound { +#if 0 + namespace game { + class StateUpdateEvent; + } + + class SoundEffect { + public: + virtual ~SoundEffects() + { + } + + // return false if it can be deleted + virtual bool advance(float dt) = 0; + }; + + class SoundEffects { + public: + SoundEffects() + { + } + + void handleUpdateEvent(game::StateUpdateEvent *event); + void advance(float dt, const std::list &updates); + + private: + std::list m_effects; + } +#endif +} diff --git a/game/state/commands.cpp b/game/state/commands.cpp index ea21632..0d60b31 100644 --- a/game/state/commands.cpp +++ b/game/state/commands.cpp @@ -14,18 +14,16 @@ namespace game { // angles are supplied in degrees and are CCW Missile *missile = new Missile( + state->generateMissileId(), player, player->ship->position, -util::deg2rad(m_angle), 0.005*player->speed); - Trace *trace = new Trace(missile); - missile->trace = trace; - player->energy -= player->speed; player->missiles.push_back(missile); - state->addTrace(trace); + state->addMissile(missile); } bool ShootCommand::ready(const Player *player, const State *state) const diff --git a/game/state/events/explosion_event.hpp b/game/state/events/explosion_event.hpp new file mode 100644 index 0000000..6c081d2 --- /dev/null +++ b/game/state/events/explosion_event.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "state/explosion.hpp" +#include "state/state_update_event.hpp" + +namespace game { + class ExplosionEvent : public StateUpdateEvent { + public: + ExplosionEvent(StateUpdateEvent::LifeCycle lifeCycle, Explosion *explosion) + : StateUpdateEvent(lifeCycle, StateUpdateEvent::EventType::Explosion) + , explosion(explosion) + + { + std::string typeStr = StateUpdateEvent::lifeCycleToString(lifeCycle); + std::cout<<"created explosion event for id " << explosion->id << " of type " << typeStr << std::endl; + } + + public: + Explosion *explosion; + }; +} diff --git a/game/state/missile.cpp b/game/state/missile.cpp index 5b7462e..f55208d 100644 --- a/game/state/missile.cpp +++ b/game/state/missile.cpp @@ -11,8 +11,9 @@ #include namespace game { - Missile::Missile(Player *player, const glm::vec2 &pos, float angle, float speed) - : player(player) + Missile::Missile(size_t id, Player *player, const glm::vec2 &pos, float angle, float speed) + : id(id) + , player(player) , position(pos) { velocity = speed * glm::vec2(std::sin(angle), std::cos(angle)); diff --git a/game/state/missile.hpp b/game/state/missile.hpp index bd7beb5..fad104c 100644 --- a/game/state/missile.hpp +++ b/game/state/missile.hpp @@ -57,17 +57,18 @@ namespace game { int planetId; }; - Missile(Player *player, const glm::vec2 &pos, float angle, float speed); + Missile(size_t id, Player *player, const glm::vec2 &pos, float angle, float speed); ~Missile(); // try to advance. if something will be hit, return the first hit in // time. Missile::Event advance(const game::State *state, float dt); - Player *player; // owner won't be hit by own missiles - glm::vec2 position; - glm::vec2 velocity; - Trace *trace; + const size_t id; + Player *player; // owner won't be hit by own missiles + glm::vec2 position; + glm::vec2 velocity; + Trace *trace; }; } diff --git a/game/state/player.hpp b/game/state/player.hpp index af58142..736a336 100644 --- a/game/state/player.hpp +++ b/game/state/player.hpp @@ -14,7 +14,7 @@ namespace game { class Player { public: - int id; + size_t id; bool alive; float speed; float energy; @@ -23,7 +23,7 @@ namespace game { std::string name; std::list missiles; - Player(int id) : id(id), alive(true), speed(1.0), energy(0.0), ship(nullptr), name("") + Player(size_t id) : id(id), alive(true), speed(1.0), energy(0.0), ship(nullptr), name("") { } diff --git a/game/state/state.cpp b/game/state/state.cpp index be541ed..935e2cc 100644 --- a/game/state/state.cpp +++ b/game/state/state.cpp @@ -15,6 +15,9 @@ #include "trace.hpp" #include "explosion.hpp" +#include "state_update_event.hpp" +#include "events/explosion_event.hpp" + #include "util.hpp" namespace game { @@ -28,7 +31,8 @@ namespace game { //for (Player *player : players) { //} - m_nextId = 0; + m_nextPlayerId = 0; + m_nextMissileId = 0; m_time = 0.0; m_shipRadius = 0.02; m_maxMissileDistance = 2.0; @@ -146,15 +150,15 @@ namespace game { return true; } - int State::addPlayer() + size_t State::addPlayer() { - int playerId = m_nextId++; + size_t playerId = m_nextPlayerId++; Player *player = new Player(playerId); players.push_back(player); return playerId; } - void State::quitPlayer(int playerId) + void State::quitPlayer(size_t playerId) { std::cout << playerId << " quit" << std::endl; @@ -168,12 +172,12 @@ namespace game { } } - void State::clear(int playerId) + void State::clear(size_t playerId) { std::cout << playerId << " clear" << std::endl; } - void State::setName(int playerId, std::string name) + void State::setName(size_t playerId, std::string name) { // discard if not unique for (const Player *other : players) { @@ -186,7 +190,7 @@ namespace game { playerForId(playerId)->name = name; } - void State::setSpeed(int playerId, double speed) + void State::setSpeed(size_t playerId, double speed) { playerForId(playerId)->speed = speed; } @@ -338,12 +342,15 @@ namespace game { for (Explosion *explosion : rm) { explosions.remove(explosion); - delete(explosion); + m_nextEvents.push_back(new ExplosionEvent(StateUpdateEvent::LifeCycle::Destroy, explosion)); + //delete(explosion); } } void State::advance(float dt) { + //std::cout<<"[state] (init) cycle: update events length is " << m_nextEvents.size() << std::endl; + m_time += dt; advancePlayerShipSpawns(dt); @@ -355,9 +362,19 @@ namespace game { advancePlayerCommands(dt); advancePlayerMissiles(dt); + + //std::cout<<"[state] (before move) cycle: update events length is " << m_nextEvents.size() << std::endl; + + // put collected events into that list. + m_allEvents.push_back(std::move(m_nextEvents)); + + //std::cout<<"[state] (after move) cycle: update events length is " << m_nextEvents.size() << std::endl; + + // finally remove things. + //m_nextEvents.clear(); } - Player *State::playerForId(int playerId) + Player *State::playerForId(size_t playerId) { for (Player *p : players) { if (p->id == playerId) { @@ -402,7 +419,7 @@ namespace game { } } - void State::commandForPlayer(int playerId, game::Command *cmd) + void State::commandForPlayer(size_t playerId, game::Command *cmd) { Player *player = playerForId(playerId); if (player != nullptr) { @@ -425,6 +442,14 @@ namespace game { traces.push_back(trace); } + void State::addMissile(Missile *missile) + { + Trace *trace = new Trace(missile); + missile->trace = trace; + + addTrace(trace); + } + void State::deleteTrace(Trace *trace) { if (trace->missile != nullptr) { @@ -442,7 +467,10 @@ namespace game { return; } - explosions.push_back(new Explosion(m_nextExplosionId++, evt->position, evt->missileVelocity, evt->hit)); + Explosion *explosion = new Explosion(m_nextExplosionId++, evt->position, evt->missileVelocity, evt->hit); + explosions.push_back(explosion); + + m_nextEvents.push_back(new ExplosionEvent(StateUpdateEvent::LifeCycle::Create, explosion)); } void State::advanceTraceAges(float dt) @@ -472,4 +500,23 @@ namespace game { { m_playingFieldSize = glm::vec2(width, height); } + + size_t State::generateMissileId() + { + return m_nextMissileId++; + } + + void State::applyAndClearAllOldStateUpdates() + { + // TODO: delete the items for events that are to be removed in proper + // way + + for (std::list list : m_allEvents) { + for (StateUpdateEvent *evt : list) { + delete(evt); + } + list.clear(); + } + m_allEvents.clear(); + } } diff --git a/game/state/state.hpp b/game/state/state.hpp index da6a656..8aed6fb 100644 --- a/game/state/state.hpp +++ b/game/state/state.hpp @@ -28,6 +28,7 @@ namespace game { // forward declarations class Command; class Player; + class StateUpdateEvent; class Ship; class Trace; @@ -53,15 +54,15 @@ namespace game { // The upper layer (network/renderer) calling these three functions // should keep id's unique and give one (network) input an id. - int addPlayer(); - void clear(int playerId); - void setName(int playerId, std::string name); - void setSpeed(int playerId, double speed); - void quitPlayer(int playerId); - void commandForPlayer(int playerId, Command *cmd); + size_t addPlayer(); + void clear(size_t playerId); + void setName(size_t playerId, std::string name); + void setSpeed(size_t playerId, double speed); + void quitPlayer(size_t playerId); + void commandForPlayer(size_t playerId, Command *cmd); // lookup. return nullptr on invalid playerId - Player *playerForId(int playerId); + Player *playerForId(size_t playerId); /*************************************************************************/ /* Mixed stuff */ @@ -75,6 +76,9 @@ namespace game { // add a trace to the list of traces. void addTrace(Trace *trace); + // add a missile + void addMissile(Missile *missile); + // delete traces with this command void deleteTrace(Trace *trace); // using a pointer @@ -97,10 +101,17 @@ namespace game { void setPlayingFieldSize(int width, int height); void setPlayingFieldCenter(int width, int height); + size_t generateMissileId(); + + void applyAndClearAllOldStateUpdates(); + + /*************************************************************************/ /* Rendering */ /*************************************************************************/ + // TODO: hide and replace by events + // Game items which should be rendered are here: // (access missiles by iterating over player's missiles attribute) std::vector planets; @@ -135,17 +146,21 @@ namespace game { // usefule for spanwing things bool findFreePositionWithRadius(float r, glm::vec2 &pos); - float m_maxMissileDistance; - float m_playerRespawnTime; - float m_shipRadius; - float m_defaultEnergy; - int m_nextId; - int m_maxNumTraces; + float m_maxMissileDistance; + float m_playerRespawnTime; + float m_shipRadius; + float m_defaultEnergy; + int m_maxNumTraces; + size_t m_nextPlayerId; size_t m_nextExplosionId; - float m_time; - bool m_developerMode; + size_t m_nextMissileId; + float m_time; + bool m_developerMode; glm::vec2 m_playingFieldCenter; glm::vec2 m_playingFieldSize; + + std::list m_nextEvents; + std::vector> m_allEvents; }; -}; +} diff --git a/game/state/state_update_event.hpp b/game/state/state_update_event.hpp new file mode 100644 index 0000000..0db7486 --- /dev/null +++ b/game/state/state_update_event.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +// TODO: make life cycle object class. +// objects from that class can be created and destroyed only through factory +// methods which create updates too. + +namespace game { + class StateUpdateEvent { + public: + enum class LifeCycle { + Create, // something was created + Modify, // something was modified (look at attributes) + Destroy // something was destroyed + }; + + // add all possible classes here + enum class EventType { + Explosion + }; + + StateUpdateEvent(LifeCycle cycle, EventType event) + : m_lifeCycle(cycle), m_eventType(event) + { + } + + LifeCycle lifeCycle() const { return m_lifeCycle; } + EventType eventType() const { return m_eventType; } + + std::string description() const + { + // TODO + return ""; + } + + static std::string lifeCycleToString(LifeCycle lifeCycle) + { + switch(lifeCycle) { + case StateUpdateEvent::LifeCycle::Create: return "create"; + case StateUpdateEvent::LifeCycle::Modify: return "modify"; + case StateUpdateEvent::LifeCycle::Destroy: return "destroy"; + default: return ""; + } + } + + private: + const LifeCycle m_lifeCycle; + const EventType m_eventType; + }; +}