// Copyright (c) 2023 Alex Diener. All rights reserved. #include "gamemath/PCGRandom.h" #include "gamemath/Scalar.h" #include "gamemath/VectorConversions.h" #include "PROJECT_NAME/Atoms.h" #include "PROJECT_NAME/Drawing.h" #include "PROJECT_NAME/EntityComponent_position.h" #include "PROJECT_NAME/EntityComponent_sprite.h" #include "PROJECT_NAME/GameView.h" #include "PROJECT_NAME/Globals.h" #include "PROJECT_NAME/Music.h" #include "PROJECT_NAME/Shaders.h" #include "PROJECT_NAME/Sprites.h" #include "PROJECT_NAME/Sounds.h" #include "PROJECT_NAME/VisualEffect_sparkle.h" #include "stem_core.h" #include #include #include #define stemobject_implementation GameView stemobject_vtable_begin(); stemobject_vtable_entry(dispose); stemobject_vtable_end(); #define TURN_INTERPOLATION_TIME (3 / 60.0) #define BUMP_OFFSET 4.0f GameView * GameView_create(GameState * gameState) { stemobject_create_implementation(init, gameState) } static Vector2f getCameraOffset(GameView * self) { Vector2f cameraOffset = VECTOR2f(-self->roomCameraPosition.x, -self->roomCameraPosition.y); if (self->roomTransition != NULL) { cameraOffset = RoomTransition_getCameraOffset(self->roomTransition); } cameraOffset.x += self->screenShakeOffset.x; cameraOffset.y += self->screenShakeOffset.y; return cameraOffset; } struct spriteDrawCommand { SpriteID spriteID; unsigned int animationTime; Vector2f screenPosition; int drawLayer; int drawPriority; }; static bool getSpriteDrawCommandForEntity(GameEntity * entity, Vector2f offset, float interpolation, double animationReferenceTime, struct spriteDrawCommand * outDrawCommand) { EntityComponent_position * positionComponent = call_virtual(getComponent, entity, COMPONENT_POSITION); EntityComponent_sprite * spriteComponent = call_virtual(getComponent, entity, COMPONENT_SPRITE); if (positionComponent != NULL && spriteComponent != NULL) { Vector2f position = worldToScreenPosition(positionComponent->position); Vector2f lastPosition; if (positionComponent->lastPosition.x == positionComponent->position.x && positionComponent->lastPosition.y == positionComponent->position.y) { lastPosition = VECTOR2f(position.x + positionComponent->lastBumpDirection.x * BUMP_OFFSET, position.y - positionComponent->lastBumpDirection.y * BUMP_OFFSET); } else { lastPosition = worldToScreenPosition(positionComponent->lastPosition); } Vector2f drawnPosition = Vector2f_interpolate(lastPosition, position, interpolation); outDrawCommand->spriteID = spriteComponent->spriteID; outDrawCommand->animationTime = call_virtual(getAnimationTime, spriteComponent, animationReferenceTime); outDrawCommand->screenPosition = Vector2f_add(drawnPosition, offset); outDrawCommand->drawLayer = spriteComponent->drawLayer; outDrawCommand->drawPriority = spriteComponent->drawPriority; return true; } return false; } static int compareSpriteDrawCommands(const void * lhsUntyped, const void * rhsUntyped) { const struct spriteDrawCommand * lhs = lhsUntyped, * rhs = rhsUntyped; if (lhs->drawLayer != rhs->drawLayer) { return (lhs->drawLayer > rhs->drawLayer) * 2 - 1; } if (lhs->screenPosition.y != rhs->screenPosition.y) { return (lhs->screenPosition.y < rhs->screenPosition.y) * 2 - 1; } if (lhs->screenPosition.x != rhs->screenPosition.x) { return (lhs->screenPosition.x > rhs->screenPosition.x) * 2 - 1; } return (lhs->drawPriority > rhs->drawPriority) * 2 - 1; } static void writeEntityVertices(Renderable * renderable, VertexIO * vertexIO, void * context) { GameView * self = context; RoomState * roomState = self->gameState->rooms[self->gameState->currentRoomIndex]; unsigned int totalEntityCount = roomState->entityCount; if (self->roomTransition != NULL) { totalEntityCount += self->gameState->rooms[self->previousRoomIndex]->entityCount; } struct spriteDrawCommand spriteDrawCommands[totalEntityCount]; Vector2f transitionCameraOffset = VECTOR2f_ZERO; if (self->roomTransition != NULL) { transitionCameraOffset = RoomTransition_getCameraOffset(self->roomTransition); } unsigned int drawnEntityCount = 0; float interpolation = sinf((1.0f - self->turnInterpolationRemainingTime / TURN_INTERPOLATION_TIME) * M_PI * 0.5f); for (unsigned int entityIndex = 0; entityIndex < roomState->entityCount; entityIndex++) { drawnEntityCount += getSpriteDrawCommandForEntity(roomState->entities[entityIndex].entity, transitionCameraOffset, interpolation, self->animationReferenceTime, &spriteDrawCommands[drawnEntityCount]); } if (self->roomTransition != NULL) { Vector2f cameraOffset = Vector2f_add(Vector2f_inverted(self->roomTransition->offsetFromOldToNew), transitionCameraOffset); RoomState * previousRoomState = self->gameState->rooms[self->previousRoomIndex]; for (unsigned int entityIndex = 0; entityIndex < previousRoomState->entityCount; entityIndex++) { drawnEntityCount += getSpriteDrawCommandForEntity(previousRoomState->entities[entityIndex].entity, cameraOffset, interpolation, self->animationReferenceTime, &spriteDrawCommands[drawnEntityCount]); } } qsort(spriteDrawCommands, drawnEntityCount, sizeof(spriteDrawCommands[0]), compareSpriteDrawCommands); for (unsigned int entityIndex = 0; entityIndex < drawnEntityCount; entityIndex++) { drawSprite(spriteDrawCommands[entityIndex].spriteID, spriteDrawCommands[entityIndex].animationTime, true, spriteDrawCommands[entityIndex].screenPosition, COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO); } if (self->mouseoverTile.x != INT_MIN) { drawSprite(SPRITE_ID_MOUSEOVER, 0, true, worldToScreenPosition(self->mouseoverTile), COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO); } } static void writeVisualEffectVertices(Renderable * renderable, VertexIO * vertexIO, void * context) { GameView * self = context; for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) { call_virtual(writeVertices, self->visualEffects[visualEffectIndex], VECTOR2f_ZERO, vertexIO); } } static void writeRoomVisualToVertexBuffer(GameView * self) { VertexIO * vertexIO = VertexIO_create(self->roomShaderConfiguration->shader->vertexFormat, 0.0, 0.0); if (self->roomTransition != NULL) { RoomTransition_draw(self->roomTransition, VECTOR2f_ZERO, vertexIO); } else { RoomVisual_draw(self->roomVisual, VECTOR2f_ZERO, vertexIO); } VertexBuffer_bufferData(self->roomVertexBuffer, vertexIO->vertices, vertexIO->vertexCount, vertexIO->indexes, vertexIO->indexCount, BUFFER_USAGE_STREAM); VertexIO_dispose(vertexIO); } static bool turnStartedCallback(Atom eventID, void * eventData, void * context) { beginSoundEffectGroup(); return true; } static bool turnEndedCallback(Atom eventID, void * eventData, void * context) { GameView * self = context; endSoundEffectGroup(); self->turnInterpolationRemainingTime = TURN_INTERPOLATION_TIME; self->mouseoverTile = VECTOR2i(INT_MIN, INT_MIN); return true; } static bool playSoundCallback(Atom eventID, void * eventData, void * context) { SoundID soundID = (uintptr_t) context; playSoundEffect(soundID); return true; } static bool pickupCollectedCallback(Atom eventID, void * eventData, void * context) { GameView * self = context; GameEntity * entity = eventData; playSoundEffect(SOUND_PICKUP); EntityComponent_position * positionComponent = call_virtual(getComponent, entity, COMPONENT_POSITION); if (positionComponent != NULL) { GameView_addVisualEffect(self, VisualEffect_sparkle_create(positionComponent->position)); } return true; } bool GameView_init(GameView * self, GameState * gameState) { call_super(init, self); self->gameState = gameState; self->roomVisual = RoomVisual_create(self->gameState->rooms[self->gameState->currentRoomIndex]); self->visualEffectCount = 0; self->visualEffectAllocatedCount = 8; self->visualEffects = malloc(self->visualEffectAllocatedCount * sizeof(*self->visualEffects)); self->screenShakeTime = 0.0; self->screenShakeRadius = 0.0f; self->screenShakeOffset = VECTOR2f_ZERO; self->roomCameraPosition = VECTOR2f_ZERO; self->animationReferenceTime = 0.0; self->turnInterpolationRemainingTime = 0.0; self->mouseoverTile = VECTOR2i(INT_MIN, INT_MIN); self->roomShaderConfiguration = ShaderConfigurationRoom_create(g_roomShader); call_virtual(setTexture, self->roomShaderConfiguration, 0, g_imageCollectionTexture, false); call_virtual(referenceProjectionMatrix, self->roomShaderConfiguration, &g_projectionUniform); self->roomVertexBuffer = VertexBuffer_create(g_roomShader->vertexFormat, NULL, 0, NULL, 0, BUFFER_USAGE_STREAM); self->roomRenderable = Renderable_createWithVertexBuffer(PRIMITIVE_TRIANGLES, &g_uiContext.defaultRenderPipelineConfiguration, self->roomShaderConfiguration, self->roomVertexBuffer, false, NULL, self); self->entityRenderable = Renderable_createWithCallback(PRIMITIVE_TRIANGLES, &g_uiContext.defaultRenderPipelineConfiguration, g_shaderConfiguration, writeEntityVertices, NULL, self); self->visualEffectRenderable = Renderable_createWithCallback(PRIMITIVE_TRIANGLES, &g_uiContext.defaultRenderPipelineConfiguration, g_shaderConfiguration, writeVisualEffectVertices, NULL, self); self->roomTransition = NULL; writeRoomVisualToVertexBuffer(self); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_turn_started, turnStartedCallback, self); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_turn_ended, turnEndedCallback, self); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_player_moved, playSoundCallback, (void *) (uintptr_t) SOUND_PLAYER_MOVE); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_player_move_failed, playSoundCallback, (void *) (uintptr_t) SOUND_PLAYER_MOVE_FAILED); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_player_bumped_wall, playSoundCallback, (void *) (uintptr_t) SOUND_PLAYER_BUMP); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_tile_destroyed, playSoundCallback, (void *) (uintptr_t) SOUND_WALL_BREAK); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_pickup_collected, pickupCollectedCallback, self); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_game_state_saved, playSoundCallback, (void *) (uintptr_t) SOUND_CHECKPOINT); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_block_pushed, playSoundCallback, (void *) (uintptr_t) SOUND_PUSH); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_enemy_killed, playSoundCallback, (void *) (uintptr_t) SOUND_ENEMY_DEATH); EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_player_killed, playSoundCallback, (void *) (uintptr_t) SOUND_PLAYER_DEATH); playMusic(self->gameState->rooms[self->gameState->currentRoomIndex]->roomData->musicID, true); return true; } void GameView_dispose(GameView * self) { EventDispatcher_unregisterAllForContext(self->gameState->eventDispatcher, self); EventDispatcher_unregisterAllForCallback(self->gameState->eventDispatcher, playSoundCallback); Renderable_dispose(self->roomRenderable); Renderable_dispose(self->entityRenderable); Renderable_dispose(self->visualEffectRenderable); VertexBuffer_dispose(self->roomVertexBuffer); call_virtual(dispose, self->roomShaderConfiguration); RoomVisual_dispose(self->roomVisual); if (self->roomTransition != NULL) { call_virtual(dispose, self->roomTransition); } for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) { call_virtual(dispose, self->visualEffects[visualEffectIndex]); } free(self->visualEffects); call_super_virtual(dispose, self); } static Vector2f getRoomCameraPosition(GameView * self, unsigned int roomIndex) { RoomData * roomData = self->gameState->rooms[roomIndex]->roomData; Rect4f roomCameraBounds = {0.0f, roomData->size.x * TILE_WIDTH - DISPLAY_WIDTH, -((int) roomData->size.y - TILE_MAP_HEIGHT) * TILE_HEIGHT, roomData->size.y * TILE_HEIGHT - DISPLAY_HEIGHT - ((int) roomData->size.y - TILE_MAP_HEIGHT) * TILE_HEIGHT}; if (roomCameraBounds.xMin > roomCameraBounds.xMax) { roomCameraBounds.xMin = roomCameraBounds.xMax = roomCameraBounds.xMin + (roomCameraBounds.xMax - roomCameraBounds.xMin) * 0.5f; } if (roomCameraBounds.yMin > roomCameraBounds.yMax) { roomCameraBounds.yMin = roomCameraBounds.yMax = roomCameraBounds.yMin + (roomCameraBounds.yMax - roomCameraBounds.yMin) * 0.5f; } Vector2f roomCameraPosition = VECTOR2f(self->gameState->player->positionComponent.position.x * TILE_WIDTH - DISPLAY_WIDTH / 2, self->gameState->player->positionComponent.position.y * TILE_HEIGHT + DISPLAY_HEIGHT / 2); return Rect4f_clampVector2f(roomCameraBounds, roomCameraPosition); } void GameView_update(GameView * self, double deltaTime) { self->animationReferenceTime += deltaTime; self->turnInterpolationRemainingTime = fmax(0.0, self->turnInterpolationRemainingTime - deltaTime); self->roomCameraPosition = Vector2f_round(getRoomCameraPosition(self, self->gameState->currentRoomIndex)); Matrix4x4f matrix = MATRIX4x4f_IDENTITY; if (self->roomTransition != NULL) { RoomTransition_advance(self->roomTransition, deltaTime); if (RoomTransition_isDone(self->roomTransition)) { RoomTransition_dispose(self->roomTransition); self->roomTransition = NULL; writeRoomVisualToVertexBuffer(self); Matrix4x4f_translate(&matrix, -self->roomCameraPosition.x, -self->roomCameraPosition.y, 0.0f); } else { Vector2f cameraOffset = RoomTransition_getCameraOffset(self->roomTransition); Matrix4x4f_translate(&matrix, cameraOffset.x, cameraOffset.y, 0.0f); } } else { Matrix4x4f_translate(&matrix, -self->roomCameraPosition.x, -self->roomCameraPosition.y, 0.0f); } if (self->screenShakeTime > 0.0) { float radius = self->screenShakeTime / self->screenShakeMaxTime * self->screenShakeRadius; float angle = pcg32_frand(&g_pcgState, M_PI); self->screenShakeOffset = VECTOR2f(roundpositivef(cosf(angle) * radius), roundpositivef(sinf(angle) * radius)); self->screenShakeTime -= deltaTime; if (self->screenShakeTime < 0.0) { self->screenShakeTime = 0.0; } Matrix4x4f_translate(&matrix, self->screenShakeOffset.x, self->screenShakeOffset.y, 0.0f); } else { self->screenShakeOffset = VECTOR2f_ZERO; } ShaderConfigurationRoom_setViewMatrix(self->roomShaderConfiguration, matrix); unsigned int visualEffectOffset = 0; for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) { self->visualEffects[visualEffectIndex] = self->visualEffects[visualEffectIndex + visualEffectOffset]; call_virtual(update, self->visualEffects[visualEffectIndex], deltaTime); if (call_virtual(isFinished, self->visualEffects[visualEffectIndex])) { call_virtual(dispose, self->visualEffects[visualEffectIndex]); visualEffectOffset++; self->visualEffectCount--; visualEffectIndex--; } } } void GameView_listRenderables(GameView * self, RenderableIO * renderableIO, int drawOrderOffset, Rect4i clipBounds) { RenderableIO_addRenderable(renderableIO, self->roomRenderable, 0, RECT4i_EMPTY); RenderableIO_addRenderable(renderableIO, self->entityRenderable, 0, RECT4i_EMPTY); RenderableIO_addRenderable(renderableIO, self->visualEffectRenderable, 0, RECT4i_EMPTY); for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) { call_virtual(listRenderables, self->visualEffects[visualEffectIndex], renderableIO, drawOrderOffset, clipBounds); } } void GameView_beginRoomTransition(GameView * self, unsigned int fromRoomIndex) { Vector2f oldRoomCameraPosition = self->roomCameraPosition; RoomVisual * oldRoomVisual = self->roomVisual; self->roomVisual = RoomVisual_create(self->gameState->rooms[self->gameState->currentRoomIndex]); self->roomCameraPosition = getRoomCameraPosition(self, self->gameState->currentRoomIndex); Vector2i oldRoomPosition = self->gameState->rooms[fromRoomIndex]->roomData->zonePosition; Vector2i newRoomPosition = self->gameState->rooms[self->gameState->currentRoomIndex]->roomData->zonePosition; float interpolationStart = 0.0f; if (self->roomTransition != NULL) { interpolationStart = 1.0f - self->roomTransition->interpolation.currentProgress; RoomTransition_dispose(self->roomTransition); } self->roomTransition = RoomTransition_create(oldRoomVisual, self->roomVisual, VECTOR2f((newRoomPosition.x - oldRoomPosition.x) * TILE_WIDTH, (oldRoomPosition.y - newRoomPosition.y) * TILE_HEIGHT), oldRoomCameraPosition, self->roomCameraPosition, interpolationStart); writeRoomVisualToVertexBuffer(self); for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) { call_virtual(dispose, self->visualEffects[visualEffectIndex]); } self->visualEffectCount = 0; self->previousRoomIndex = fromRoomIndex; playMusic(self->gameState->rooms[self->gameState->currentRoomIndex]->roomData->musicID, true); } bool GameView_mouseMoved(GameView * self, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { Vector2i mouseoverTile = VECTOR2i(floorf(x / TILE_WIDTH), floorf((DISPLAY_HEIGHT - y) / TILE_HEIGHT)); if (mouseoverTile.x < 0 || mouseoverTile.y < 0 || mouseoverTile.x >= TILE_MAP_WIDTH || mouseoverTile.y >= TILE_MAP_HEIGHT) { mouseoverTile = VECTOR2i(INT_MIN, INT_MIN); } if (mouseoverTile.x != self->mouseoverTile.x || mouseoverTile.y != self->mouseoverTile.y) { self->mouseoverTile = mouseoverTile; return true; } return false; } void GameView_hideMouseover(GameView * self) { self->mouseoverTile = VECTOR2i(INT_MIN, INT_MIN); } void GameView_addVisualEffect(GameView * self, compat_type(VisualEffect *) visualEffect) { if (self->visualEffectCount >= self->visualEffectAllocatedCount) { self->visualEffectAllocatedCount *= 2; self->visualEffects = realloc(self->visualEffects, self->visualEffectAllocatedCount * sizeof(*self->visualEffects)); } self->visualEffects[self->visualEffectCount++] = visualEffect; } void GameView_shakeScreen(GameView * self, double duration, float radius) { if (self->screenShakeTime == 0.0) { self->screenShakeTime = duration; self->screenShakeMaxTime = duration; self->screenShakeRadius = radius; } else { if (self->screenShakeTime < duration) { self->screenShakeTime = duration; self->screenShakeMaxTime = duration; } if (radius > self->screenShakeRadius) { self->screenShakeRadius = radius; } } } Vector2f GameView_getPlayerScreenPosition(GameView * self) { Vector2f cameraOffset = getCameraOffset(self); Vector2f position = worldToScreenPosition(self->gameState->player->positionComponent.position); return VECTOR2f(position.x + cameraOffset.x, position.y + cameraOffset.y); } Rect4f GameView_getVisibleScreenBounds(GameView * self) { Vector2f cameraOffset = Vector2f_inverted(getCameraOffset(self)); return Rect4f_fromPositionAndSize(VECTOR2f_TRUNCATE(cameraOffset), VECTOR2f(DISPLAY_WIDTH, DISPLAY_HEIGHT)); }