Maintain all subtitles, limit calls to state.update

This commit is contained in:
Isse 2023-05-03 13:05:54 +02:00
parent f60c44d812
commit 54a7367cb2
3 changed files with 215 additions and 112 deletions

View File

@ -25,8 +25,8 @@ use crate::hud::Subtitle;
#[derive(Default, Clone)]
pub struct Listener {
pos: Vec3<f32>,
ori: Vec3<f32>,
pub pos: Vec3<f32>,
pub ori: Vec3<f32>,
ear_left_rpos: Vec3<f32>,
ear_right_rpos: Vec3<f32>,
@ -64,7 +64,8 @@ pub struct AudioFrontend {
impl AudioFrontend {
/// Construct with given device
pub fn new(
/* dev: String, */ num_sfx_channels: usize,
/* dev: String, */
num_sfx_channels: usize,
num_ui_channels: usize,
subtitles: bool,
) -> Self {
@ -268,15 +269,7 @@ impl AudioFrontend {
underwater: bool,
) {
if let Some((sfx_file, dur, subtitle)) = Self::get_sfx_file(trigger_item) {
if self.subtitles_enabled {
if let Some(subtitle) = subtitle {
self.subtitles.push_back(Subtitle {
localization: subtitle.to_string(),
position: Some(position),
show_until: dur.max(1.5) as f64,
})
}
}
self.emit_subtitle(subtitle, Some(position), dur);
// Play sound in empty channel at given position
if self.audio_stream.is_some() && self.sfx_enabled() {
let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0));
@ -310,15 +303,7 @@ impl AudioFrontend {
underwater: bool,
) {
if let Some((sfx_file, dur, subtitle)) = Self::get_sfx_file(trigger_item) {
if self.subtitles_enabled {
if let Some(subtitle) = subtitle {
self.subtitles.push_back(Subtitle {
localization: subtitle.to_string(),
position: Some(position),
show_until: dur.max(1.5) as f64,
})
}
}
self.emit_subtitle(subtitle, Some(position), dur);
// Play sound in empty channel at given position
if self.audio_stream.is_some() && self.sfx_enabled() {
let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0));
@ -353,15 +338,7 @@ impl AudioFrontend {
volume: Option<f32>,
) {
if let Some((sfx_file, dur, subtitle)) = Self::get_sfx_file(trigger_item) {
if self.subtitles_enabled {
if let Some(subtitle) = subtitle {
self.subtitles.push_back(Subtitle {
localization: subtitle.to_string(),
position: None,
show_until: dur.max(1.5) as f64,
})
}
}
self.emit_subtitle(subtitle, None, dur);
// Play sound in empty channel
if self.audio_stream.is_some() && self.sfx_enabled() {
let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0));
@ -374,6 +351,26 @@ impl AudioFrontend {
}
}
pub fn emit_subtitle(
&mut self,
subtitle: Option<&str>,
position: Option<Vec3<f32>>,
duration: f32,
) {
if self.subtitles_enabled {
if let Some(subtitle) = subtitle {
self.subtitles.push_back(Subtitle {
localization: subtitle.to_string(),
position,
show_for: duration as f64,
});
if self.subtitles.len() > 10 {
self.subtitles.pop_front();
}
}
}
}
/// Plays a file at a given volume in the channel with a given tag
fn play_ambient(&mut self, channel_tag: AmbientChannelTag, sound: &str, volume: Option<f32>) {
if self.audio_stream.is_some() {
@ -485,6 +482,8 @@ impl AudioFrontend {
}
}
pub fn get_listener(&self) -> &Listener { &self.listener }
/// Switches the playing music to the title music, which is pinned to a
/// specific sound file (veloren_title_tune.ogg)
pub fn play_title_music(&mut self) {

View File

@ -3352,13 +3352,12 @@ impl Hud {
Subtitles::new(
client,
&global_state.settings,
&global_state.audio.get_listener().clone(),
&mut global_state.audio.subtitles,
&self.fonts,
i18n,
)
.set(self.ids.subtitles, ui_widgets);
} else {
global_state.audio.subtitles.clear();
}
self.new_messages = VecDeque::new();

View File

@ -1,8 +1,7 @@
use std::collections::VecDeque;
use std::{cmp::Ordering, collections::VecDeque};
use crate::{settings::Settings, ui::fonts::Fonts};
use crate::{audio::Listener, settings::Settings, ui::fonts::Fonts};
use client::Client;
use common::comp;
use conrod_core::{
widget::{self, Id, Rectangle, Text},
widget_ids, Colorable, Positionable, UiCell, Widget, WidgetCommon,
@ -23,6 +22,7 @@ widget_ids! {
pub struct Subtitles<'a> {
client: &'a Client,
settings: &'a Settings,
listener: &'a Listener,
fonts: &'a Fonts,
@ -38,6 +38,7 @@ impl<'a> Subtitles<'a> {
pub fn new(
client: &'a Client,
settings: &'a Settings,
listener: &'a Listener,
new_subtitles: &'a mut VecDeque<Subtitle>,
fonts: &'a Fonts,
localized_strings: &'a Localization,
@ -45,6 +46,7 @@ impl<'a> Subtitles<'a> {
Self {
client,
settings,
listener,
fonts,
new_subtitles,
common: widget::CommonBuilder::default(),
@ -53,17 +55,115 @@ impl<'a> Subtitles<'a> {
}
}
const MIN_SUBTITLE_DURATION: f64 = 1.5;
const MAX_SUBTITLE_DIST: f32 = 80.0;
#[derive(Debug)]
pub struct Subtitle {
pub localization: String,
/// Position the sound is played at, if any.
pub position: Option<Vec3<f32>>,
pub show_until: f64,
/// Amount of seconds to show the subtitle for.
pub show_for: f64,
}
#[derive(Clone, PartialEq)]
struct SubtitleData {
position: Option<Vec3<f32>>,
/// `Time` to show until.
show_until: f64,
}
impl SubtitleData {
/// Prioritize showing nearby sounds, and secondarily prioritize longer
/// living sounds.
fn compare_priority(&self, other: &Self, listener_pos: Vec3<f32>) -> Ordering {
let life_cmp = self
.show_until
.partial_cmp(&other.show_until)
.unwrap_or(Ordering::Equal);
match (self.position, other.position) {
(Some(a), Some(b)) => match a
.distance_squared(listener_pos)
.partial_cmp(&b.distance_squared(listener_pos))
.unwrap_or(Ordering::Equal)
{
Ordering::Equal => life_cmp,
Ordering::Less => Ordering::Greater,
Ordering::Greater => Ordering::Less,
},
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => life_cmp,
}
}
}
#[derive(Clone)]
struct SubtitleList {
subtitles: Vec<(String, Vec<SubtitleData>)>,
}
impl SubtitleList {
fn new() -> Self {
Self {
subtitles: Vec::new(),
}
}
/// Updates the subtitle state, returns the amount of subtitles that should
/// be displayed.
fn update(
&mut self,
new_subtitles: impl Iterator<Item = Subtitle>,
time: f64,
listener_pos: Vec3<f32>,
) -> usize {
for subtitle in new_subtitles {
let show_until = time + subtitle.show_for.max(MIN_SUBTITLE_DURATION);
let data = SubtitleData {
position: subtitle.position,
show_until,
};
if let Some((_, datas)) = self
.subtitles
.iter_mut()
.find(|(key, _)| key == &subtitle.localization)
{
datas.push(data);
} else {
self.subtitles.push((subtitle.localization, vec![data]))
}
}
let mut to_display = 0;
self.subtitles.retain_mut(|(_, data)| {
data.retain(|subtitle| subtitle.show_until > time);
// Place the most prioritized subtitle in the back.
if let Some((i, s)) = data
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.compare_priority(b, listener_pos))
{
// We only display subtitles that are in range.
if s.position.map_or(true, |pos| {
pos.distance_squared(listener_pos) < MAX_SUBTITLE_DIST * MAX_SUBTITLE_DIST
}) {
to_display += 1;
}
let last = data.len() - 1;
data.swap(i, last);
true
} else {
// If data is empty we have no sounds with this key.
false
}
});
to_display
}
}
pub struct State {
subtitles: Vec<Subtitle>,
subtitles: SubtitleList,
ids: Ids,
}
@ -74,7 +174,7 @@ impl<'a> Widget for Subtitles<'a> {
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
State {
subtitles: Vec::new(),
subtitles: SubtitleList::new(),
ids: Ids::new(id_gen),
}
}
@ -86,79 +186,71 @@ impl<'a> Widget for Subtitles<'a> {
let widget::UpdateArgs { state, ui, .. } = args;
let time = self.client.state().get_time();
let player_pos = self.client.position().unwrap_or_default();
let player_dir = self
.client
.state()
.read_storage::<comp::Ori>()
.get(self.client.entity())
.map_or(Vec3::unit_y(), |ori| ori.look_vec());
// Empty old subtitles and add new.
state.update(|s| {
s.subtitles.retain(|subtitle| {
time <= subtitle.show_until
&& subtitle
.position
.map_or(true, |pos| pos.distance(player_pos) <= MAX_SUBTITLE_DIST)
});
for mut subtitle in self.new_subtitles.drain(..) {
if subtitle
.position
.map_or(false, |pos| pos.distance(player_pos) > MAX_SUBTITLE_DIST)
{
continue;
}
let t = time + subtitle.show_until;
if let Some(s) = s
.subtitles
.iter_mut()
.find(|s| s.localization == subtitle.localization)
{
if t > s.show_until {
s.show_until = t;
s.position = subtitle.position;
}
} else {
subtitle.show_until = t;
s.subtitles.push(subtitle);
}
}
s.ids
.subtitle_message
.resize(s.subtitles.len(), &mut ui.widget_id_generator());
s.ids
.subtitle_dir
.resize(s.subtitles.len(), &mut ui.widget_id_generator());
});
let listener_pos = self.listener.pos;
let listener_forward = self.listener.ori;
let color = |t: &Subtitle| {
// Update subtitles and look for changes
let mut subtitles = state.subtitles.clone();
let has_new = !self.new_subtitles.is_empty();
let show_count = subtitles.update(self.new_subtitles.drain(..), time, listener_pos);
let subtitles = if has_new || show_count != state.ids.subtitle_message.len() {
state.update(|s| {
s.subtitles = subtitles;
s.ids
.subtitle_message
.resize(show_count, &mut ui.widget_id_generator());
s.ids
.subtitle_dir
.resize(show_count, &mut ui.widget_id_generator());
});
&state.subtitles
} else {
&subtitles
};
let color = |t: &SubtitleData| -> conrod_core::Color {
conrod_core::Color::Rgba(
0.9,
1.0,
1.0,
((t.show_until - time) * 1.5).clamp(0.0, 1.0) as f32,
((t.show_until - time) * 2.0).clamp(0.0, 1.0) as f32,
)
};
let player_dir = player_dir.xy().try_normalized().unwrap_or(Vec2::unit_y());
let player_right = Vec2::new(player_dir.y, -player_dir.x);
let listener_forward = listener_forward
.xy()
.try_normalized()
.unwrap_or(Vec2::unit_y());
let listener_right = Vec2::new(listener_forward.y, -listener_forward.x);
let message = |subtitle: &Subtitle| self.localized_strings.get_msg(&subtitle.localization);
let dir = |subtitle: &SubtitleData, id: &Id, dir_id: &Id, ui: &mut UiCell| {
enum Side {
/// Also used for sounds without direction.
Forward,
Right,
Left,
}
let is_right = subtitle
.position
.map(|pos| {
let dist = pos.distance(listener_pos);
let dir = (pos - listener_pos) / dist;
let dir = |subtitle: &Subtitle, id: &Id, dir_id: &Id, ui: &mut UiCell| {
let is_right = subtitle.position.and_then(|pos| {
let dist = pos.distance(player_pos);
let dir = (pos - player_pos) / dist;
let dot = dir.xy().dot(listener_forward);
if dist < 2.0 || dot > 0.85 {
Side::Forward
} else if dir.xy().dot(listener_right) >= 0.0 {
Side::Right
} else {
Side::Left
}
})
.unwrap_or(Side::Forward);
let dot = dir.xy().dot(player_dir);
if dist < 2.0 || dot > 0.85 {
None
} else {
Some(dir.xy().dot(player_right) >= 0.0)
}
});
match is_right {
Some(true) => Text::new("> ")
Side::Right => Text::new("> ")
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
@ -166,7 +258,7 @@ impl<'a> Widget for Subtitles<'a> {
.align_middle_y_of(*id)
.color(color(subtitle))
.set(*dir_id, ui),
Some(false) => Text::new(" <")
Side::Left => Text::new(" <")
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
@ -174,7 +266,7 @@ impl<'a> Widget for Subtitles<'a> {
.align_middle_y_of(*id)
.color(color(subtitle))
.set(*dir_id, ui),
None => Text::new("")
Side::Forward => Text::new("")
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
@ -183,9 +275,9 @@ impl<'a> Widget for Subtitles<'a> {
}
};
Rectangle::fill([200.0, 2.0 + 22.0 * state.subtitles.len() as f64])
Rectangle::fill([200.0, 22.0 * show_count as f64])
.rgba(0.0, 0.0, 0.0, self.settings.chat.chat_opacity)
.bottom_right_with_margins_on(ui.window, 40.0, 50.0)
.bottom_right_with_margins_on(ui.window, 40.0, 30.0)
.set(state.ids.subtitle_box_bg, ui);
let mut subtitles = state
@ -193,32 +285,45 @@ impl<'a> Widget for Subtitles<'a> {
.subtitle_message
.iter()
.zip(state.ids.subtitle_dir.iter())
.zip(state.subtitles.iter());
.zip(
subtitles
.subtitles
.iter()
.filter_map(|(localization, data)| {
Some((self.localized_strings.get_msg(localization), data.last()?))
})
.filter(|(_, data)| {
data.position.map_or(true, |pos| {
pos.distance_squared(listener_pos)
< MAX_SUBTITLE_DIST * MAX_SUBTITLE_DIST
})
}),
);
if let Some(((id, dir_id), subtitle)) = subtitles.next() {
Text::new(&message(subtitle))
if let Some(((id, dir_id), (message, data))) = subtitles.next() {
Text::new(&message)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
.center_justify()
.mid_bottom_with_margin_on(state.ids.subtitle_box_bg, 6.0)
.color(color(subtitle))
.color(color(data))
.set(*id, ui);
dir(subtitle, id, dir_id, ui);
dir(data, id, dir_id, ui);
let mut last_id = *id;
for ((id, dir_id), subtitle) in subtitles {
Text::new(&message(subtitle))
for ((id, dir_id), (message, data)) in subtitles {
Text::new(&message)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
.up_from(last_id, 8.0)
.align_middle_x_of(last_id)
.color(color(subtitle))
.color(color(data))
.set(*id, ui);
dir(subtitle, id, dir_id, ui);
dir(data, id, dir_id, ui);
last_id = *id;
}