Änderung: Hinzufügung der MotorSound-Klasse und Integration in das Taxi-Spiel

Änderungen:
- Implementierung eines neuen AudioWorklet-Prozessors für realistische Motorgeräusche.
- Erstellung der MotorSound-Klasse zur Verwaltung von motorgeräuschabhängigen Audioeffekten.
- Integration des MotorSounds in die TaxiGame.vue, einschließlich der Initialisierung und Steuerung basierend auf der Geschwindigkeit des Taxis.
- Anpassung der Audio-Parameter wie Lautstärke und Geschwindigkeit in Abhängigkeit von der Fahrzeuggeschwindigkeit.

Diese Anpassungen verbessern die akustische Benutzererfahrung im Taxi-Minispiel erheblich und tragen zur Immersion bei.
This commit is contained in:
Torsten Schulz (local)
2025-09-17 10:00:00 +02:00
parent a19e255ca7
commit 37174c7237
3 changed files with 300 additions and 3 deletions

View File

@@ -0,0 +1,43 @@
// AudioWorklet Processor für MotorSound
class MotorSoundProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.data = null;
this.speed = 0.6;
this.currentFrame = 0;
this.port.onmessage = (e) => {
const { type, data, speed } = e.data;
switch (type) {
case 'init':
this.data = data;
this.speed = speed;
break;
case 'updateData':
this.data = data;
break;
case 'updateSpeed':
this.speed = speed;
break;
}
};
}
process(inputs, outputs, parameters) {
const output = outputs[0];
if (!this.data || this.data.length === 0) return true;
for (let channel = 0; channel < output.length; channel++) {
const outputChannel = output[channel];
for (let i = 0; i < outputChannel.length; i++) {
this.currentFrame += this.speed;
const index = Math.floor(this.currentFrame) % this.data.length;
outputChannel[i] = this.data[index];
}
}
this.currentFrame %= this.data.length;
return true;
}
}
registerProcessor('motor-sound-processor', MotorSoundProcessor);

View File

@@ -0,0 +1,195 @@
// Moderne MotorSound-Lib mit Web Audio API (AudioWorklet statt ScriptProcessor)
// Geschwindigkeitsabhängiges Motorgeräusch für Taxi-Spiel
class MotorSound {
constructor(context, generator) {
this.context = context;
this.generator = generator;
this.speed = 0.6; // incoming control value (0.3..1.5)
this.isPlaying = false;
this.currentFrame = 0;
this.workletNode = null;
this.gainNode = null;
this.data = null;
this.sampleRate = context.sampleRate || 44100;
this.step = 1; // table index increment per sample, derived from target frequency
this.regenerate();
}
async init() {
// Verwende ScriptProcessor für bessere Kompatibilität
console.log('Initialisiere MotorSound mit ScriptProcessor');
this.workletNode = this.context.createScriptProcessor(1024);
this.workletNode.onaudioprocess = this.process.bind(this);
// Filter-Kette für motorähnlicheren Klang
this.lowShelf = this.context.createBiquadFilter();
this.lowShelf.type = 'lowshelf';
this.lowShelf.frequency.value = 150;
this.lowShelf.gain.value = 6;
this.lowPass = this.context.createBiquadFilter();
this.lowPass.type = 'lowpass';
this.lowPass.frequency.value = 1200;
this.lowPass.Q.value = 0.5;
this.gainNode = this.context.createGain();
this.gainNode.gain.value = 0.25; // Leiser starten
// Verbindung: script -> lowshelf -> lowpass -> gain
this.workletNode.connect(this.lowShelf);
this.lowShelf.connect(this.lowPass);
this.lowPass.connect(this.gainNode);
console.log('MotorSound ScriptProcessor initialisiert');
}
start() {
if (!this.gainNode) return;
if (this.context.state === 'suspended') {
this.context.resume().catch(() => {});
}
this.gainNode.connect(this.context.destination);
this.isPlaying = true;
}
stop() {
if (!this.gainNode) return;
this.gainNode.disconnect(this.context.destination);
this.isPlaying = false;
}
regenerate() {
this.data = this.generator.generate();
console.log('MotorSound Daten regeneriert, Länge:', this.data.length);
}
setVolume(volume) {
if (this.gainNode) this.gainNode.gain.value = volume;
}
setGenerator(generator) {
this.generator = generator;
this.regenerate();
}
setSpeed(speed) {
// speed erwartet ca. 0.3..1.5 (von Spiel geliefert). Normiere auf 0..1
const normalized = Math.max(0, Math.min(1, (speed - 0.3) / 1.2));
// Ziel-Frequenz für Motorgrundton (Hz)
const freq = 40 + normalized * 160; // 40..200 Hz
// Tabellen-Schritt für gewünschte Frequenz
this.step = (freq * this.data.length) / this.sampleRate;
}
process(event) {
if (!this.data || this.data.length === 0) return;
const output = event.outputBuffer;
const ch0 = output.getChannelData(0);
const ch1 = output.numberOfChannels > 1 ? output.getChannelData(1) : ch0;
for (let i = 0; i < ch0.length; ++i) {
this.currentFrame += this.step;
const index = Math.floor(this.currentFrame) % this.data.length;
const sample = this.data[index] * 0.9; // etwas dämpfen
ch0[i] = sample;
ch1[i] = sample;
}
this.currentFrame %= this.data.length;
}
}
class LinearGenerator {
constructor() {
this.dataLength = 1024;
}
pushLinear(data, toValue, toPosition) {
const lastPosition = data.length - 1;
const lastValue = data.pop();
const positionDiff = toPosition - lastPosition;
const step = (toValue - lastValue) / positionDiff;
for (let i = 0; i < positionDiff; i++) {
data.push(lastValue + step * i);
}
return data;
}
generate() {
const data = [];
let lastValue = 1;
data.push(lastValue);
for (let i = 0.05; i < 1; i += Math.random() / 8 + 0.01) {
const nextPosition = Math.floor(i * this.dataLength);
const nextValue = Math.random() * 2 - 1;
this.pushLinear(data, nextValue, nextPosition);
}
this.pushLinear(data, 1, this.dataLength);
return data;
}
}
class NoiseGenerator {
constructor() {
this.dataLength = 4096;
this.linearLength = 30;
this.smoothness = 3;
}
generate() {
const data = [];
let lastValue = 0.5;
data.push(lastValue);
for (let i = 1; i <= this.dataLength - this.linearLength; i++) {
lastValue += (Math.random() - 0.5) / this.smoothness;
lastValue = Math.min(1, lastValue);
lastValue = Math.max(-1, lastValue);
data.push(lastValue);
}
const step = (0.5 - lastValue) / this.linearLength;
for (let j = 0; j < this.linearLength; j++) {
data.push(lastValue + step * j);
}
data.push(0.5);
return data;
}
}
class CanvasGenerator {
constructor() {
this.canvas = document.createElement('canvas');
this.canvas.width = 1024;
this.canvas.height = 1;
this.ctx = this.canvas.getContext('2d');
}
getRandomGradient() {
const gradient = this.ctx.createLinearGradient(0, 0, this.canvas.width, 0);
gradient.addColorStop(0, "rgba(0, 0, 0, 255)");
for (let i = 0.05; i < 1; i += Math.random() / 8 + 0.01) {
gradient.addColorStop(i, "rgba(0, 0, 0," + Math.random() + ")");
}
gradient.addColorStop(1, "rgba(0, 0, 0, 255)");
return gradient;
}
generate() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = this.getRandomGradient();
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, 1).data;
const data = [];
for (let i = 3, len = imageData.length; i < len; i += 4) {
data.push(imageData[i] / 128 - 1);
}
return data;
}
}
export { MotorSound, LinearGenerator, NoiseGenerator, CanvasGenerator };

View File

@@ -170,6 +170,7 @@
<script>
import streetCoordinates from '../../utils/streetCoordinates.js';
import { MotorSound, NoiseGenerator } from '../../assets/motorSound.js';
import apiClient from '../../utils/axios.js';
import StatusBar from '../../components/falukant/StatusBar.vue';
@@ -224,16 +225,19 @@ export default {
passengers: [],
destinations: [],
obstacles: [],
keys: {}
keys: {},
motorSound: null,
audioContext: null
}
},
mounted() {
async mounted() {
this.initializeGame();
this.initializeMinimap();
this.loadTiles();
this.loadTaxiImage();
this.loadMaps();
this.setupEventListeners();
await this.initializeMotorSound();
},
beforeUnmount() {
this.cleanup();
@@ -304,10 +308,58 @@ export default {
document.addEventListener('keyup', this.handleKeyUp);
},
async initializeMotorSound() {
// AudioContext wird erst bei erster Benutzerinteraktion erstellt
console.log('MotorSound wird bei erster Benutzerinteraktion initialisiert');
},
async initAudioOnUserInteraction() {
if (this.audioContext) return; // Bereits initialisiert
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
const generator = new NoiseGenerator();
this.motorSound = new MotorSound(this.audioContext, generator);
await this.motorSound.init();
console.log('MotorSound initialisiert');
} catch (error) {
console.warn('MotorSound konnte nicht initialisiert werden:', error);
}
},
updateMotorSound() {
if (!this.motorSound) return;
const speedKmh = this.taxi.speed * 5; // Geschwindigkeit in km/h
const isMoving = speedKmh > 0;
if (isMoving && !this.motorSound.isPlaying) {
this.motorSound.start();
} else if (!isMoving && this.motorSound.isPlaying) {
this.motorSound.stop();
}
if (isMoving) {
// Geschwindigkeitsabhängige Tonhöhe und Lautstärke
const speedFactor = Math.min(speedKmh / 120, 1); // 0-1 basierend auf 0-120 km/h
const motorSpeed = 0.3 + (speedFactor * 1.2); // 0.3 bis 1.5
const volume = 0.1 + (speedFactor * 0.4); // 0.1 bis 0.5
this.motorSound.setSpeed(motorSpeed);
this.motorSound.setVolume(volume);
}
},
cleanup() {
if (this.gameLoop) {
cancelAnimationFrame(this.gameLoop);
}
if (this.motorSound) {
this.motorSound.stop();
}
if (this.audioContext) {
this.audioContext.close();
}
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
},
@@ -495,6 +547,9 @@ export default {
if (Math.abs(this.taxi.speed) > 0.1) {
this.fuel = Math.max(0, this.fuel - 0.01);
}
// Motorgeräusch aktualisieren
this.updateMotorSound();
},
handlePassengerActions() {
@@ -940,8 +995,12 @@ export default {
// Canvas-Klick-Handling falls benötigt
},
handleKeyDown(event) {
async handleKeyDown(event) {
this.keys[event.key] = true;
// AudioContext bei erster Benutzerinteraktion initialisieren
await this.initAudioOnUserInteraction();
event.preventDefault();
},