/********************************************************************************************** * * raylib - Advance Game template * * Gameplay Screen Functions Definitions (Init, Update, Draw, Unload) * * Copyright (c) 2014 Ramon Santamaria (Ray San - raysan@raysanweb.com) * * This software is provided "as-is", without any express or implied warranty. In no event * will the authors be held liable for any damages arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, including commercial * applications, and to alter it and redistribute it freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not claim that you * wrote the original software. If you use this software in a product, an acknowledgment * in the product documentation would be appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not be misrepresented * as being the original software. * * 3. This notice may not be removed or altered from any source distribution. * **********************************************************************************************/ #include "raylib.h" #include "screens.h" #include // Required for: malloc(), free() #include // Required for: sqrtf(), asinf() #define MAX_SAMPLES_SPEED 7 // Max speed for samples movement #define MIN_SAMPLES_SPEED 3 // Min speed for samples movement #define SAMPLES_SPACING 100 // Separation between samples in pixels #define SAMPLES_MULTIPLIER 700 // Defines sample data scaling value (it would be adjusted to MAX_GAME_HEIGHT) #define MAX_GAME_HEIGHT 400 // Defines max possible amplitude between samples (game area) //---------------------------------------------------------------------------------- // Types and Structures Definition //---------------------------------------------------------------------------------- typedef struct Player { Vector2 position; Vector2 speed; int width; int height; Color color; } Player; typedef struct Sample { Vector2 position; float value; // Raw audio sample value (normalized) int radius; bool active; // Define if sample is active (can be collected) bool collected; // Define if sample has been collected bool renderable; // Define if sample should be rendered Color color; } Sample; //---------------------------------------------------------------------------------- // Global Variables Definition (local to this module) //---------------------------------------------------------------------------------- // Gameplay screen global variables static int framesCounter; static int finishScreen; static bool pause; // Player variables static Player player; static Rectangle playerArea; // Define player movement area (and sample collection limits) static float warpCounter; // Time warp counter static float synchro; // Calculates collected samples relation [0..1] static int combo; static int maxCombo; static Rectangle waveRec; // Samples variables static Sample *samples; // Game samples static int totalSamples = 0; // Total game samples (proportional to waveData num samples) static int collectedSamples; // Samples collected by player static int currentSample; // Last sample to go through player collect area static float samplesSpeed; // All samples move at the same speed static float waveTime; // Total sample time in ms // Resources variables static Texture2D texBackground; static Texture2D texPlayer; static Texture2D texSampleSmall; static Texture2D texSampleMid; static Texture2D texSampleBig; static Texture2D texLine; static RenderTexture2D waveTarget; static Sound fxSampleOn; // Collected sample sound static Sound fxSampleOff; // Miss sample sound static Sound fxPause; // Pause sound // Debug variables //------------------------------------------------------------------------------------ // Module Functions Declaration (local) //------------------------------------------------------------------------------------ static void DrawSamples(Sample *samples, int sampleCount, int playedSamples, Rectangle bounds, Color color); //---------------------------------------------------------------------------------- // Gameplay Screen Functions Definition //---------------------------------------------------------------------------------- // Gameplay Screen Initialization logic void InitGameplayScreen(void) { framesCounter = 0; finishScreen = 0; pause = false; endingStatus = 0; // Textures loading texBackground = LoadTexture("resources/textures/background_gameplay.png"); texPlayer = LoadTexture("resources/textures/player.png"); texSampleSmall = LoadTexture("resources/textures/sample_small.png"); texSampleMid = LoadTexture("resources/textures/sample_mid.png"); texSampleBig = LoadTexture("resources/textures/sample_big.png"); texLine = LoadTexture("resources/textures/line.png"); waveRec = (Rectangle){ 32, 32, 1280 - 64, 105 }; waveTarget = LoadRenderTexture(waveRec.width, waveRec.height); // Sound loading fxSampleOn = LoadSound("resources/audio/sample_on.wav"); fxSampleOff = LoadSound("resources/audio/sample_off.wav"); fxPause = LoadSound("resources/audio/pause.wav"); SetSoundVolume(fxSampleOn, 0.6f); SetSoundVolume(fxPause, 0.5f); // Initialize player data playerArea = (Rectangle){ 200, 160, 80, 400 }; player.width = 20; player.height = 60; player.speed = (Vector2){ 15, 15 }; player.color = GOLD; player.position = (Vector2){ playerArea.x + playerArea.width/2 - texPlayer.width/2, playerArea.y + playerArea.height/2 - texPlayer.height/2 }; warpCounter = 395; synchro = 0.2f; combo = 0; maxCombo = 0; // Initialize wave and samples data Wave wave = LoadWave("resources/audio/wave.ogg"); float *waveData = GetWaveData(wave); // TODO: Be careful with channels! // We calculate the required parameters to adjust audio time to gameplay time // that way samples collected correspond to audio playing // Synchonization is not perfect due to possible rounding issues (float to int) waveTime = wave.sampleCount/wave.sampleRate; // Total sample time in seconds float requiredSamples = (MAX_SAMPLES_SPEED*waveTime*60 - 1000)/SAMPLES_SPACING; int samplesDivision = (int)(wave.sampleCount/requiredSamples); totalSamples = wave.sampleCount/samplesDivision; // We don't need wave any more (already got waveData) UnloadWave(wave); collectedSamples = 0; // Init samples samples = (Sample *)malloc(totalSamples*sizeof(Sample)); // Normalize wave data (min vs max values) to scale properly float minSampleValue = 0.0f; float maxSampleValue = 0.0f; for (int i = 0; i < totalSamples; i++) { if (waveData[i*samplesDivision] < minSampleValue) minSampleValue = waveData[i*samplesDivision]; if (waveData[i*samplesDivision] > maxSampleValue) maxSampleValue = waveData[i*samplesDivision]; } float sampleScaleFactor = 1.0f/(maxSampleValue - minSampleValue); // 400 pixels maximum size // Initialize samples for (int i = 0; i < totalSamples; i++) { samples[i].value = waveData[i*samplesDivision]*sampleScaleFactor; // Normalized value [-1.0..1.0] samples[i].position.x = player.position.x + 1000 + i*SAMPLES_SPACING; samples[i].position.y = GetScreenHeight()/2 + samples[i].value*SAMPLES_MULTIPLIER; if (samples[i].position.y > GetScreenHeight()/2 + MAX_GAME_HEIGHT/2) samples[i].position.y = GetScreenHeight()/2 - MAX_GAME_HEIGHT/2; else if (samples[i].position.y < GetScreenHeight()/2 - MAX_GAME_HEIGHT/2) samples[i].position.y = GetScreenHeight()/2 + MAX_GAME_HEIGHT/2; samples[i].radius = 6; samples[i].active = true; samples[i].collected = false; samples[i].color = RED; } samplesSpeed = MAX_SAMPLES_SPEED; currentSample = 0; // We already saved the samples we needed for the game, we can free waveData free(waveData); // Load and start playing music // NOTE: Music is loaded in main code base PlayMusicStream(music); } // Gameplay Screen Update logic void UpdateGameplayScreen(void) { if (IsKeyPressed('P')) { PlaySound(fxPause); pause = !pause; if (pause) PauseMusicStream(music); else ResumeMusicStream(music); } if (!pause) { framesCounter++; // Time starts counting to awake enemies // Player movement logic (mouse) player.position.y = GetMousePosition().y; // Player movement logic (keyboard) if (IsKeyDown(KEY_W)) player.position.y -= player.speed.y; else if (IsKeyDown(KEY_S)) player.position.y += player.speed.y; // Player movement logic (gamepad) /* if (IsGamepadAvailable(GAMEPAD_PLAYER1)) { Vector2 movement = { 0.0f }; movement.x = GetGamepadAxisMovement(GAMEPAD_PLAYER1, GAMEPAD_PS3_AXIS_LEFT_X); movement.y = GetGamepadAxisMovement(GAMEPAD_PLAYER1, GAMEPAD_PS3_AXIS_LEFT_Y); player.position.x += movement.x*0.1f; // Scale gamepad movement value player.position.y += movement.y*0.1f; // Scale gamepad movement value } */ // Player logic: check player area limits if (player.position.x < playerArea.x) player.position.x = playerArea.x; else if ((player.position.x + player.width) > (playerArea.x + playerArea.width)) player.position.x = playerArea.x + playerArea.width - player.width; if (player.position.y < playerArea.y) player.position.y = playerArea.y; else if ((player.position.y + player.height) > (playerArea.y + playerArea.height)) player.position.y = playerArea.y + playerArea.height - player.height; // Samples logic for (int i = 0; i < totalSamples; i++) { // Samples movement logic samples[i].position.x -= samplesSpeed; if (((samples[i].position.x + samples[i].radius) > -SAMPLES_SPACING) && ((samples[i].position.x - samples[i].radius) < GetScreenWidth())) samples[i].renderable = true; else samples[i].renderable = false; // Samples catch logic if (!samples[i].collected && CheckCollisionCircleRec(samples[i].position, samples[i].radius, (Rectangle){ (int)player.position.x, (int)player.position.y, player.width, player.height })) { samples[i].collected = true; collectedSamples++; synchro += 0.02; combo++; if (combo > maxCombo) maxCombo = combo; if (synchro >= 1.0f) synchro = 1.0f; // Set sound pitch depending on sample position (base pitch: 1.0f) // NOTE: waveData[i*WAVE_SAMPLES_DIV] is scaled to [0.3..1.7] SetSoundPitch(fxSampleOn, samples[i].value*1.4f + 0.7f); PlaySound(fxSampleOn); } if ((samples[i].position.x - samples[i].radius) < player.position.x) { currentSample = i; // Register last sample going out range if (samples[i].active) { samples[i].active = false; if (!samples[i].collected) { synchro -= 0.05f; PlaySound(fxSampleOff); combo = 0; } } } } if (IsKeyDown(KEY_SPACE) && (warpCounter > 0)) { warpCounter--; if (warpCounter < 0) warpCounter = 0; samplesSpeed -= 0.1f; if (samplesSpeed <= MIN_SAMPLES_SPEED) samplesSpeed = MIN_SAMPLES_SPEED; SetMusicPitch(music, samplesSpeed/MAX_SAMPLES_SPEED); } else { warpCounter++; if (warpCounter > 395) warpCounter = 395; samplesSpeed += 0.1f; if (samplesSpeed >= MAX_SAMPLES_SPEED) samplesSpeed = MAX_SAMPLES_SPEED; SetMusicPitch(music, samplesSpeed/MAX_SAMPLES_SPEED); } // Check ending conditions if (currentSample >= totalSamples - 1) { StopMusicStream(music); endingStatus = 1; // Win finishScreen = 1; } if (synchro <= 0.0f) { synchro = 0.0f; StopMusicStream(music); endingStatus = 2; // Loose finishScreen = 1; } } } // Gameplay Screen Draw logic void DrawGameplayScreen(void) { // Draw background DrawTexture(texBackground, 0, 0, WHITE); // Screen elements drawing //DrawRectangleLines(playerArea.x, playerArea.y, playerArea.width, playerArea.height, BLUE); DrawRectangle(0, GetScreenHeight()/2 - 1, GetScreenWidth(), 2, Fade(BLUE, 0.3f)); //DrawRectangleLines(0, GetScreenHeight()/2 - MAX_GAME_HEIGHT/2, GetScreenWidth(), MAX_GAME_HEIGHT, GRAY); // Draw samples for (int i = 0; i < totalSamples - 1; i++) { if (samples[i].renderable) { Color col = samples[i].color; if (i < (currentSample + 1)) col = Fade(DARKGRAY, 0.5f); else col = WHITE; //DrawCircleV(samples[i].position, samples[i].radius, col); if (!samples[i].collected) { if (combo > 50) DrawTexture(texSampleSmall, samples[i].position.x - texSampleSmall.width/2, samples[i].position.y - texSampleSmall.height/2, col); else if (combo > 25) DrawTexture(texSampleMid, samples[i].position.x - texSampleMid.width/2, samples[i].position.y - texSampleMid.height/2, col); else DrawTexture(texSampleBig, samples[i].position.x - texSampleSmall.width/2, samples[i].position.y - texSampleBig.height/2, col); } if (i < (currentSample + 1)) col = Fade(GRAY, 0.3f); else col = Fade(WHITE, 0.5f); // Draw line between samples //DrawLine(samples[i].position.x, samples[i].position.y, samples[i + 1].position.x, samples[i + 1].position.y, col); float dx = samples[i + 1].position.x - samples[i].position.x; float dy = samples[i + 1].position.y - samples[i].position.y; float d = sqrtf(dx*dx + dy*dy); float angle = asinf(dy/d); // Draw lines using textures //DrawTextureEx(texLine, (Vector2){ samples[i].position.x - 2, samples[i].position.y - 2 }, -RAD2DEG*angle, d/SAMPLES_SPACING, col); // Draw lines using textures - IMPROVED // TODO: Further improving to draw lines properly DrawTexturePro(texLine, (Rectangle){ 0, 0, texLine.width, texLine.height }, (Rectangle){ samples[i].position.x, samples[i].position.y, (float)texLine.width*d/SAMPLES_SPACING, texLine.height }, (Vector2){ 0, (float)texLine.height/2 }, -RAD2DEG*angle, col); } } // Draw player //DrawRectangle((int)player.position.x, (int)player.position.y, player.width, player.height, player.color); DrawTexture(texPlayer, player.position.x - 32, player.position.y - 24, WHITE); // Draw pause message if (pause) DrawTextEx(font, "WAVE PAUSED", (Vector2){ 235, 400 }, font.baseSize*2, 0, WHITE); // Draw number of samples //DrawText(FormatText("%05i", collectedSamples), 900, 200, 40, GRAY); //DrawText(FormatText("%05i", totalSamples), 900, 250, 40, GRAY); DrawTextEx(font, FormatText("%05i / %05i", collectedSamples, totalSamples), (Vector2){810, 170}, font.baseSize, -2, SKYBLUE); // Draw combo DrawTextEx(font, FormatText("Combo: %02i [max: %02i]", combo, maxCombo), (Vector2){200, 170}, font.baseSize/2, -2, SKYBLUE); // Draw synchonicity level DrawRectangle(99, 622, 395, 32, Fade(RAYWHITE, 0.8f)); if (synchro <= 0.3f) DrawRectangle(99, 622, synchro*395, 32, Fade(RED, 0.8f)); else if (synchro <= 0.8f) DrawRectangle(99, 622, synchro*395, 32, Fade(ORANGE,0.8f)); else if (synchro < 1.0f) DrawRectangle(99, 622, synchro*395, 32, Fade(LIME,0.8f)); else DrawRectangle(99, 622, synchro*395, 32, Fade(GREEN, 0.9f)); DrawRectangleLines(99, 622, 395, 32, MAROON); if (synchro == 1.0f) DrawTextEx(font, FormatText("%02i%%", (int)(synchro*100)), (Vector2){99 + 390, 600}, font.baseSize, -2, GREEN); else DrawTextEx(font, FormatText("%02i%%", (int)(synchro*100)), (Vector2){99 + 390, 600}, font.baseSize, -2, SKYBLUE); // Draw time warp coool-down bar DrawRectangle(754, 622, 395, 32, Fade(RAYWHITE, 0.8f)); DrawRectangle(754, 622, warpCounter, 32, Fade(SKYBLUE, 0.8f)); DrawRectangleLines(754, 622, 395, 32, DARKGRAY); //DrawText(FormatText("%02i%%", (int)(synchro*100)), 754 + 410, 628, 20, DARKGRAY); DrawTextEx(font, FormatText("%02i%%", (int)((float)warpCounter/395.0f*100.0f)), (Vector2){754 + 390, 600}, font.baseSize, -2, SKYBLUE); // Draw wave // NOTE: Old drawing method, replaced by rendertarget //DrawSamples(samples, totalSamples, currentSample, waveRec, MAROON); //DrawRectangle(waveRec.x + (int)currentSample*1240/totalSamples, waveRec.y, 2, 99, DARKGRAY); //DrawRectangleLines(20, 20, 1240, 140, DARKGRAY); //DrawRectangle(20, 150, (float)currentSample/totalSamples*1240, 10, GRAY); // Draw wave using render target ClearBackground(BLANK); BeginTextureMode(waveTarget); DrawSamples(samples, totalSamples, currentSample, (Rectangle){ 0, 0, waveTarget.texture.width, waveTarget.texture.height }, MAROON); EndTextureMode(); // TODO: Apply antialiasing shader DrawTextureEx(waveTarget.texture, (Vector2){ waveRec.x, waveRec.y }, 0.0f, 1.0f, WHITE); DrawRectangle(waveRec.x + (int)currentSample*1215/totalSamples, waveRec.y, 2, 99, DARKGRAY); } // Gameplay Screen Unload logic void UnloadGameplayScreen(void) { // Unload textures UnloadTexture(texBackground); UnloadTexture(texPlayer); UnloadTexture(texSampleSmall); UnloadTexture(texSampleMid); UnloadTexture(texSampleBig); UnloadTexture(texLine); UnloadRenderTexture(waveTarget); // Unload sounds UnloadSound(fxSampleOn); UnloadSound(fxSampleOff); UnloadSound(fxPause); free(samples); // Unload game samples (crashes game) } // Gameplay Screen should finish? int FinishGameplayScreen(void) { return finishScreen; } //------------------------------------------------------------------------------------ // Module Functions Definitions (local) //------------------------------------------------------------------------------------ // Draw samples in wave form (including already played samples in a different color!) // NOTE: For proper visualization, MSAA x4 is recommended, alternatively // it should be rendered to a bigger texture and then scaled down with // bilinear/trilinear texture filtering static void DrawSamples(Sample *samples, int sampleCount, int playedSamples, Rectangle bounds, Color color) { // NOTE: We just pick a sample to draw every increment float sampleIncrementX = (float)bounds.width/sampleCount; Color col = color; for (int i = 0; i < sampleCount - 1; i++) { if (i < playedSamples) col = GRAY; else col = color; DrawLineV((Vector2){ (float)bounds.x + (float)i*sampleIncrementX, (float)(bounds.y + bounds.height/2) + samples[i].value*bounds.height }, (Vector2){ (float)bounds.x + (float)(i + 1)*sampleIncrementX, (float)(bounds.y + bounds.height/2) + + samples[i + 1].value*bounds.height }, col); } }