High-Level Structure of 'Harmony With Me' Game
Game Design Idea
The game is an interactive music experience where users create harmonious sounds by tapping a circle that appears at random positions on the screen. The game features background music, haptic feedback, and inactivity monitoring to enhance the user experience.
Key Components
-
File Management
- Uses
FileSystemManager
for audio resource handling - Checks for essential files like "testBGM.m4a"
- Uses
-
Audio System
- Managed through
AudioManager
- Handles background music and harmony sounds
- Managed through
-
User Interaction
- Uses
DragGesture
for touch detection - Implements circle-based touch area detection
- Includes haptic feedback via
HapticManager
- Uses
-
State Management
- Tracks visibility state (
showHomeButton
) - Monitors gameplay state
- Handles cleanup on view disappear
- Tracks visibility state (
Control Flow in A Nutshell
graph TD
A[App Launch] --> B[Initialize Components]
B --> C[Start BGM]
B --> D[Setup Touch Detection]
C --> E[Wait for User Input]
E --> F{Touch Inside Circle?}
F -->|Yes| G[Play Harmony Sound]
G --> H[Trigger Haptics]
F -->|No| E
E --> I[Monitor Inactivity]
I -->|Inactive| J[Show Home Button]
-
Initialization
- On view appearance, the game checks for necessary audio files, configures the audio session, prepares haptics, starts background music, and initializes the game state.
-
User Interaction
- Users interact with the game by tapping the circle when it appears. The game detects touches inside the circle, plays a random harmony sound, triggers haptic feedback, and resets the inactivity timer.
-
Background Music and Circle Display
- Background music plays in a loop, and the game monitors the playback position to display the interactive circle at predefined times. The circle appears at random positions and disappears after a set duration.
-
Inactivity Monitoring
- The game monitors user inactivity and displays a home button if the user is inactive for a certain period. The inactivity timer resets with each user interaction.
-
Cleanup
- On view disappearance, the game fades out the audio, stops timers, and cleans up resources.
Main View Structure
-
Imports and Declarations
- Import necessary frameworks:
SwiftUI
,AVFoundation
,CoreHaptics
- Define
MusicDelegate
class for handling background music looping
- Import necessary frameworks:
-
Main View:
HarmonyWithMe
- State properties for managing game state, audio players, timers, and UI elements
- Constants for game configuration (e.g., circle duration, sound groups)
-
body
ViewNavigationStack
withGeometryReader
for responsive layoutZStack
for layering UI elements:- Background color
- Instruction text
- Interactive circle
- Home button
-
Lifecycle Handlers
.onAppear
to initialize game components and start background music.gesture
to handle touch interactions.onDisappear
to clean up resources
-
Helper Methods
startBackgroundMusic()
: Initialize and start background music with loopinginitializeGame()
: Set up initial game state and start timersplayRandomSoundFromCurrentGroup()
: Play a random sound from the current sound groupisPlayingHarmonySound()
: Check if a harmony sound is currently playingisTouchInsideCircle(at:)
: Determine if a touch is inside the interactive circleshowCircleAtRandomPosition()
: Display the interactive circle at a random positionstartMusicTimer()
: Monitor background music playback positioncheckMusicPosition()
: Check if it's time to show the interactive circlestartInactivityTimer()
: Start a timer to monitor user inactivitystopInactivityTimer()
: Stop the inactivity timerresetInactivityTimer()
: Reset the inactivity timer and hide the home buttoncheckForInactivity()
: Check if the user has been inactive for a certain periodresetGame()
: Reset the game statefadeOutAndClean()
: Fade out audio and clean up resources
Control Flows in Detail
Initialization (onAppear)
graph TD
A[View Appears] --> B[Debug File System Check]
B --> C[Verify BGM File]
C --> D[Configure Audio]
D --> E[Setup Haptics]
E --> F[Start BGM]
F --> G[Initialize Game]
BGM Playback Position Monitoring (in the context of game flow)
Control Flow of the Music Timer
graph TD
A[Start Music Timer] --> B[Timer Tick (0.1s)]
B --> C[Check Music Position]
C --> D{Is Music About to Loop?}
D -->|Yes| E[Reset Game]
E --> F[Start Music Timer]
D -->|No| G[Check Circle Trigger Times]
G --> H{Is Current Time >= Next Trigger Time?}
H -->|Yes| I[Show Circle at Random Position]
I --> J[Increment Trigger Index]
H -->|No| K[Continue Monitoring]
J --> K
F --> K
Playback monitoring Explanation within the Game Flow
The primary purpose of checkMusicPosition()
is to determine when the background music reaches certain points in its playback timeline, known as "trigger times." At these trigger times, the function attempts to display a circle at a random position on the screen, which the user can interact with.
Call Chain, Timing and Complete Mechanism
// Initial setup in startBackgroundMusic()
musicDelegate = MusicDelegate {
DispatchQueue.main.async {
self.resetGame() // This eventually leads to startMusicTimer()
}
}
musicTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {
...
self.checkMusicPosition() // Called every 0.1 seconds
}
...
A. Timer-Based Monitoring
-
When the game starts or resets:
initializeGame()
is called- This calls
startMusicTimer()
- The timer is set to fire every 0.1 seconds
-
The timer continuously:
- Checks if the music is about to loop
- Calls
checkMusicPosition()
B. Checking Logic (function explanation)
private func checkMusicPosition() {
guard let player = backgroundPlayer,
nextTriggerIndex < circleTriggerTimes.count else { return }
let currentTime = player.currentTime
let nextTriggerTime = circleTriggerTimes[nextTriggerIndex]
let tolerance: TimeInterval = 0.2
if currentTime >= nextTriggerTime && currentTime <= nextTriggerTime + tolerance &&
!isShowingCircle {
print("⭕️ Attempting to show circle at trigger time: \(nextTriggerTime)")
currentSoundGroupIndex = nextTriggerIndex
showCircleAtRandomPosition()
nextTriggerIndex += 1
}
}
a. Guard Statement:
- The function begins with a
guard
statement to ensure that thebackgroundPlayer
is notnil
and thatnextTriggerIndex
is within the bounds of thecircleTriggerTimes
array. If either condition fails, the function exits early.
b. Current Time and Next Trigger Time:
currentTime
is obtained from thebackgroundPlayer
, representing the current playback position of the music.nextTriggerTime
is retrieved from thecircleTriggerTimes
array usingnextTriggerIndex
. This represents the next point in time when an action should be triggered.
c. Tolerance:
- A
tolerance
of 0.2 seconds is defined. This allows for a small window of time around the exact trigger time to account for any minor discrepancies in timing.
d. Trigger Condition:
- The function checks if the
currentTime
is within the range defined bynextTriggerTime
andnextTriggerTime + tolerance
. - It also checks that
isShowingCircle
isfalse
, ensuring that a circle is not already being displayed.
e. Action on Trigger:
- If the conditions are met, the function prints a message indicating that it is attempting to show a circle at the trigger time.
currentSoundGroupIndex
is updated tonextTriggerIndex
, which is used to determine which sound group to play when the circle is interacted with.showCircleAtRandomPosition()
is called to display a circle at a random position on the screen.nextTriggerIndex
is incremented to prepare for the next trigger time.
C. Game State Management
Trigger Times:
private let circleTriggerTimes: [TimeInterval] = [ ... ] //Predefined times when circles should appear
Sound Groups:
private let soundGroups: [[String]] = [
["soundname1", "soundname2"], // Group 1
... // ... more groups. Each trigger point corresponds to a sound group
]
Complete Flow
i. Game Initialization:
- Game starts →
initializeGame()
→startMusicTimer()
- Background music begins playing
ii. Continuous Monitoring:
- Every 0.1 seconds:
- Timer fires
- Checks music position
- Calls
checkMusicPosition()
iii. Circle Trigger System:
- When
currentTime
matches a trigger time (within tolerance):- Updates
currentSoundGroupIndex
- Shows circle at random position
- Prepares for next trigger by incrementing
nextTriggerIndex
- Updates
iv. Music Loop Handling:
- When music is about to end:
- MusicDelegate's closure is called
- Game resets through
resetGame()
- Timer restarts through
startMusicTimer()
Error Handling and Guards
- Guards against:
- Missing background player
- Invalid trigger indices
- Multiple circles showing simultaneously
- Out-of-bounds array access
State Management
The function manages several state variables:
nextTriggerIndex
: Tracks next circle appearancecurrentSoundGroupIndex
: Controls available soundsisShowingCircle
: Prevents multiple circles- Various animation states for circle display
This mechanism creates a synchronized experience where:
- Music plays continuously
- Circles appear at specific musical moments
- User interactions trigger appropriate sounds
- The game loops seamlessly when music ends
The 0.1-second interval for checking provides a good balance between accuracy and performance, while the 0.2-second tolerance allows for slight timing variations without affecting the user experience.
Circle Appearance Tapping Handling
graph TD
A[Check Music Position] --> B{Is Current Time >= Next Trigger Time?}
B -->|Yes| C[Show Circle at Random Position]
C --> D[Set Circle Active]
D --> E[Start Circle Disappearance Timer]
E --> F[Wait for Circle Duration]
F --> G[Hide Circle]
G --> H[Set Circle Inactive]
B -->|No| I[Continue Monitoring]
J[User Taps Circle] --> K{Is Circle Active?}
K -->|Yes| L[Play Random Sound from Current Group]
L --> M[Trigger Haptic Feedback]
M --> N[Reset Inactivity Timer]
K -->|No| O[No Action]
Explanation
Circle Appearance Handling
-
Check Music Position
- The current music playback position is checked against predefined trigger times.
-
Is Current Time >= Next Trigger Time?
- If the current playback time is greater than or equal to the next trigger time, the circle is shown at a random position.
-
Show Circle at Random Position
- The interactive circle is displayed at a random position on the screen.
-
Set Circle Active
- The circle is marked as active, allowing it to respond to user taps.
-
Start Circle Disappearance Timer
- A timer is started to hide the circle after a set duration.
-
Wait for Circle Duration
- The system waits for the circle's duration to elapse.
-
Hide Circle
- The circle is hidden after the duration elapses.
-
Set Circle Inactive
- The circle is marked as inactive, preventing it from responding to user taps.
-
Continue Monitoring
- If the current time is not yet at the next trigger time, the system continues monitoring the music position.
Circle Tapping Handling
-
User Taps Circle
- The user taps the circle on the screen.
-
Is Circle Active?
- The system checks if the circle is currently active.
-
Play Random Sound from Current Group
- If the circle is active, a random sound from the current sound group is played.
-
Trigger Haptic Feedback
- Haptic feedback is triggered to provide tactile feedback to the user.
-
Reset Inactivity Timer
- The inactivity timer is reset to prevent the home button from appearing.
-
No Action
- If the circle is not active, no action is taken.
User Activity Monitoring
graph TD
A[Start Inactivity Timer] --> B[Timer Tick (1s)]
B --> C[Check For Inactivity]
C --> D{Time Since Last Interaction >= 10s?}
D -->|Yes| E[Show Home Button]
E --> F[Update UI State]
D -->|No| G[Continue Monitoring]
F --> G
H[User Interaction] --> I[Reset Inactivity Timer]
I --> J[Update Last Interaction Time]
J --> K[Hide Home Button]
K --> G
Explanation
-
Start Inactivity Timer
- A timer is started to check for user inactivity every 1 second.
-
Timer Tick (1s)
- On each tick, the time since the last user interaction is checked.
-
Check For Inactivity
- The time elapsed since the last interaction is calculated.
-
Time Since Last Interaction >= 10s?
- If the elapsed time is greater than or equal to 10 seconds, the home button is shown.
-
Show Home Button
- The home button is made visible with an animation.
-
Update UI State
- The UI state is updated to reflect the visibility of the home button.
-
Continue Monitoring
- The timer continues to monitor user inactivity.
-
User Interaction
- When the user interacts with the game (e.g., touches the screen), the inactivity timer is reset.
-
Reset Inactivity Timer
- The last interaction time is updated, and the home button is hidden.
-
Update Last Interaction Time
- The last interaction time is set to the current time.
-
Hide Home Button
- The home button is hidden with an animation.
The Self-defiend Class MusicDelegate
Overview
The MusicDelegate
class is a custom delegate for handling background music playback events in the HarmonyWithMe
game. It conforms to the AVAudioPlayerDelegate
protocol and is primarily responsible for detecting when the background music has finished playing and triggering appropriate actions to reset the game state and loop the music.
How It Works
-
Initialization
- The
MusicDelegate
is initialized with a closure (onMusicLoop
) that defines the actions to be taken when the background music finishes playing. - This closure is stored in the
onMusicLoop
property.
- The
-
Conforming to
AVAudioPlayerDelegate
- The
MusicDelegate
class conforms to theAVAudioPlayerDelegate
protocol, which requires the implementation of theaudioPlayerDidFinishPlaying(_:successfully:)
method.
- The
-
Handling Music Playback Completion
- The
audioPlayerDidFinishPlaying(_:successfully:)
method is called automatically by theAVAudioPlayer
instance when the music finishes playing. - The method checks if the playback finished successfully using the
flag
parameter. - If the playback finished successfully, the method prints a debug message and calls the
onMusicLoop
closure to reset the game and restart the music.
- The
Usage in the main view
The MusicDelegate
is used in the HarmonyWithMe
view to handle background music looping and game state resetting.
@State private var musicDelegate: MusicDelegate?
private func startBackgroundMusic() {
backgroundPlayer = AudioManager.createAudioPlayer(filename: "testBGM", fileExtension: "m4a")
guard let player = backgroundPlayer else {
print("❌ Failed to create background player")
return
}
AudioManager.fadeIn(player)
musicDelegate = MusicDelegate {
DispatchQueue.main.async {
self.resetGame()
self.startMusicTimer()
}
}
player.delegate = musicDelegate
player.numberOfLoops = -1
}
-
Creating the Audio Player
- The
startBackgroundMusic()
method creates anAVAudioPlayer
instance for the background music.
- The
-
Setting the Delegate
- A
MusicDelegate
instance is created with a closure that resets the game and restarts the music timer. - The
musicDelegate
property is assigned this instance. - The
AVAudioPlayer
instance'sdelegate
property is set to themusicDelegate
.
- A
-
Handling Music Looping
- When the background music finishes playing, the
audioPlayerDidFinishPlaying(_:successfully:)
method of theMusicDelegate
is called. - The
onMusicLoop
closure is executed, which resets the game state and restarts the music timer.
- When the background music finishes playing, the
control flow
graph TD
A[Create MusicDelegate Instance] --> B[Initialize MusicDelegate with Closure]
B --> C[Assign MusicDelegate to AVAudioPlayer]
C --> D[Start Background Music]
D --> E[Music Playback Completes]
E --> F{Was Playback Successful?}
F -->|Yes| G[Execute onMusicLoop Closure]
G --> H[Reset Game]
G --> I[Start Music Timer]
F -->|No| J[Do Nothing]
The MusicDelegate
class is a crucial component in the HarmonyWithMe
game, ensuring that the background music loops seamlessly and the game state is reset appropriately. By conforming to the AVAudioPlayerDelegate
protocol and using a closure to define the actions on music completion, it provides a flexible and efficient way to manage background music playback and game state transitions.