Example #1
0
	entity_handle create_character(cosmos& world, const components::transform spawn_transform, const vec2i screen_size, const std::string name, const assets::animation_response_id torso_set) {
		const auto character = world.create_entity(name);

		name_entity(character, entity_name::PERSON);

		const auto crosshair = create_character_crosshair(world, screen_size);
		crosshair.get<components::crosshair>().character_entity_to_chase = character;
		crosshair.set_logic_transform(spawn_transform.pos);

		ingredients::wsad_character(character, crosshair, torso_set);
		
		auto& element = character += components::gui_element();
		element.rect_world.last_state.screen_size = screen_size;

		ingredients::wsad_character_physics(character);

		character.get<components::physics>().set_transform(spawn_transform);

		ingredients::character_inventory(character);

		const auto corpse_of_sentience = world.create_entity("corpse_of_sentience");
		name_entity(corpse_of_sentience, entity_name::CORPSE);
		ingredients::wsad_character_corpse(corpse_of_sentience);

		character.map_sub_entity(sub_entity_name::CORPSE_OF_SENTIENCE, corpse_of_sentience);

		character.add_standard_components();

		// LOG("Character mass: %x", character.get<components::physics>().get_mass());
		return character;
	}
Example #2
0
void viewing_session::advance_audiovisual_systems(
	const cosmos& cosm, 
	const entity_id viewed_character,
	const augs::variable_delta dt
) {
	auto& interp = systems_audiovisual.get<interpolation_system>();

	cosm.profiler.start(meter_type::INTERPOLATION);
	interp.integrate_interpolated_transforms(cosm, dt, dt.get_fixed());
	cosm.profiler.stop(meter_type::INTERPOLATION);

	systems_audiovisual.get<particles_simulation_system>().advance_visible_streams_and_all_particles(
		camera.smoothed_camera, 
		cosm, 
		dt, 
		interp);
	
	auto listener_cone = camera.smoothed_camera;
	listener_cone.transform = cosm[viewed_character].viewing_transform(interp);

	systems_audiovisual.get<sound_system>().play_nearby_sound_existences(
		listener_cone,
		viewed_character,
		cosm,
		cosm.get_total_time_passed_in_seconds() + dt.seconds_after_last_step(),
		interp
	);
}
Example #3
0
	entity_handle create_brick_wall(cosmos& world, const components::transform pos, const vec2 size) {
		const auto crate = world.create_entity("brick_wall");

		ingredients::sprite_scalled(crate, pos, size, assets::texture_id::BRICK_WALL, white, render_layer::DYNAMIC_BODY);
		ingredients::standard_static_body(crate);
		crate.get<components::fixtures>().set_restitution(0.0f);
		crate.get<components::fixtures>().set_density(100);
		crate.add_standard_components();

		return crate;
	}
Example #4
0
	entity_handle create_sample_backpack(cosmos& world, vec2 pos) {
		auto def = world.create_entity("sample_backpack");

		name_entity(def, entity_name::VIOLET_BACKPACK);
		ingredients::backpack(def);

		ingredients::sprite(def, pos, assets::texture_id::BACKPACK, augs::white, render_layer::SMALL_DYNAMIC_BODY);
		ingredients::see_through_dynamic_body(def);
		
		def.add_standard_components();
		return def;
	}
Example #5
0
	entity_handle create_crate(cosmos& world, const components::transform pos, const vec2 size) {
		const auto crate = world.create_entity("crate");

		name_entity(crate, entity_name::CRATE);
		ingredients::sprite_scalled(crate, pos, size, assets::texture_id::CRATE, white, render_layer::DYNAMIC_BODY);
		ingredients::standard_dynamic_body(crate, true);
		crate.get<components::fixtures>().set_restitution(0.8f);
		crate.get<components::fixtures>().set_density(0.03f);
		crate.add_standard_components();

		return crate;
	}
Example #6
0
void interpolation_system::integrate_interpolated_transforms(const cosmos& cosm, const augs::delta variable_delta, const augs::delta fixed_delta_for_slowdowns) {
	if (!enabled) {
		return;
	}
	
	const auto seconds = variable_delta.in_seconds();
	const float slowdown_multipliers_decrease = seconds / fixed_delta_for_slowdowns.in_seconds();

	for (const auto e : cosm.get(processing_subjects::WITH_INTERPOLATION)) {
		const auto& actual = e.logic_transform();
		const auto& info = e.get<components::interpolation>();
		auto& integrated = get_interpolated(e);
		auto& cache = per_entity_cache[make_cache_id(e)];

		const float considered_positional_speed = interpolation_speed / (sqrt(cache.positional_slowdown_multiplier));
		const float considered_rotational_speed = interpolation_speed / (sqrt(cache.rotational_slowdown_multiplier));

		if (cache.positional_slowdown_multiplier > 1.f) {
			cache.positional_slowdown_multiplier -= slowdown_multipliers_decrease / 4;

			if (cache.positional_slowdown_multiplier < 1.f) {
				cache.positional_slowdown_multiplier = 1.f;
			}
		}

		if (cache.rotational_slowdown_multiplier > 1.f) {
			cache.rotational_slowdown_multiplier -= slowdown_multipliers_decrease / 4;

			if (cache.rotational_slowdown_multiplier < 1.f) {
				cache.rotational_slowdown_multiplier = 1.f;
			}
		}

		const float positional_averaging_constant = 1.0f - static_cast<float>(pow(info.base_exponent, considered_positional_speed * seconds));
		const float rotational_averaging_constant = 1.0f - static_cast<float>(pow(info.base_exponent, considered_rotational_speed * seconds));

		auto& recorded_pob = cache.recorded_place_of_birth;
		auto& recorded_ver = cache.recorded_version;
		const auto& pob = info.place_of_birth;
		const auto& ver = e.get_id().pool.version;

		if (recorded_pob == pob && recorded_ver == ver) {
			integrated = actual.interpolated_separate(integrated, positional_averaging_constant, rotational_averaging_constant);
		}
		else {
			integrated = actual;
			recorded_pob = pob;
			recorded_ver = ver;
		}
	}
}
Example #7
0
void simulation_receiver::remote_entropy_predictions(guid_mapped_entropy& adjusted_entropy, const entity_id& predictable_entity, const cosmos& predicted_cosmos) {
	entity_intent release_intent;
	release_intent.is_pressed = false;

	for (auto e : predicted_cosmos.get(processing_subjects::WITH_PAST_CONTAGIOUS)) {
		const bool is_locally_controlled_entity = e == predictable_entity;
		
		if (is_locally_controlled_entity)
			continue;

		for (const auto g : e.guns_wielded()) {
			if (g.get<components::gun>().trigger_pressed) {
				if (g.get_current_slot().raw_id.type == slot_function::PRIMARY_HAND) {
					release_intent.intent = intent_type::CROSSHAIR_PRIMARY_ACTION;
				}
				else {
					release_intent.intent = intent_type::CROSSHAIR_SECONDARY_ACTION;
				}

				adjusted_entropy.entropy_per_entity[e.get_guid()].push_back(release_intent);
			}
		}
	}
}
Example #8
0
	void networked_testbed::populate_world_with_entities(cosmos& cosm) {
		cosm.advance_deterministic_schemata(cosmic_entropy(), [this](const logic_step step) { populate(step); }, [](const const_logic_step) {});
	}
Example #9
0
void movement_system::apply_movement_forces(cosmos& cosmos) {
	auto& physics_sys = cosmos.systems_temporary.get<physics_system>();
	const auto& delta = cosmos.get_fixed_delta();

	for (const auto& it : cosmos.get(processing_subjects::WITH_MOVEMENT)) {
		auto& movement = it.get<components::movement>();

		if (!movement.apply_movement_forces) {
			continue;
		}

		vec2 resultant;

		resultant.x = movement.moving_right * movement.input_acceleration_axes.x - movement.moving_left * movement.input_acceleration_axes.x;
		resultant.y = movement.moving_backward * movement.input_acceleration_axes.y - movement.moving_forward * movement.input_acceleration_axes.y;

		if (!it.has<components::physics>()) {
			it.get<components::transform>().pos += resultant * delta.in_seconds();
			continue;
		}

		const auto& physics = it.get<components::physics>();
		
		if (!physics.is_constructed())
			continue;

		if (movement.make_inert_for_ms > 0.f) {
			movement.make_inert_for_ms -= static_cast<float>(delta.in_milliseconds());
			physics.set_linear_damping(2);
		}
		else {
			physics.set_linear_damping(movement.sprint_enabled ? (movement.standard_linear_damping / 4.f) : movement.standard_linear_damping);
		}

		if (resultant.non_zero()) {
			if (movement.acceleration_length > 0) {
				resultant.set_length(movement.acceleration_length);
			}
			
			if (movement.make_inert_for_ms > 0.f) {
				resultant /= 10.f;
			}

			if (movement.sprint_enabled) {
				resultant /= 2.f;
			}
			
			if (movement.walking_enabled) {
				resultant /= 2.f;
			}

			physics.apply_force(resultant * physics.get_mass(), movement.applied_force_offset, true);
		}

		/* the player feels less like a physical projectile if we brake per-axis */
		if (movement.enable_braking_damping && !(movement.make_inert_for_ms > 0.f)) {
			physics.set_linear_damping_vec(vec2(
				resultant.x_non_zero() ? 0.f : movement.braking_damping,
				resultant.y_non_zero() ? 0.f : movement.braking_damping));
		}
		else {
			physics.set_linear_damping_vec(vec2(0, 0));
		}
	}
}
Example #10
0
void viewing_session::view(
	augs::renderer& renderer,
	const cosmos& cosmos,
	const entity_id viewed_character,
	const augs::variable_delta& dt,
	const augs::gui::text::fstr& custom_log,
	const game_drawing_settings settings
	) {
	frame_profiler.new_measurement();

	const auto screen_size = camera.smoothed_camera.visible_world_area;
	const vec2i screen_size_i(static_cast<int>(screen_size.x), static_cast<int>(screen_size.y));

	renderer.set_viewport({ viewport_coordinates.x, viewport_coordinates.y, screen_size_i.x, screen_size_i.y });

	const auto character_chased_by_camera = cosmos[viewed_character];

	camera.tick(systems_audiovisual.get<interpolation_system>(), dt, character_chased_by_camera);
	world_hover_highlighter.cycle_duration_ms = 700;
	world_hover_highlighter.update(dt.in_milliseconds());

	const auto visible = visible_entities(camera.smoothed_camera, cosmos);

	auto main_cosmos_viewing_step = viewing_step(
		cosmos, 
		*this, 
		dt, 
		renderer, 
		camera.smoothed_camera, 
		character_chased_by_camera,
		visible
	);

	main_cosmos_viewing_step.settings = settings;

#if NDEBUG || _DEBUG
	rendering_scripts::illuminated_rendering(main_cosmos_viewing_step);
#else
	rendering_scripts::illuminated_rendering(main_cosmos_viewing_step);
#endif

	using namespace augs::gui::text;

	if (show_profile_details) {
		const auto coords = character_chased_by_camera.alive() ? character_chased_by_camera.logic_transform().pos : vec2();
		const auto rot = character_chased_by_camera.alive() ? character_chased_by_camera.logic_transform().rotation : 0.f;
		const auto vel = character_chased_by_camera.alive() ? character_chased_by_camera.get<components::physics>().velocity() : vec2();

		const auto bbox = quick_print_format(renderer.triangles, typesafe_sprintf(L"Entities: %x\nX: %f2\nY: %f2\nRot: %f2\nVelX: %x\nVelY: %x\n", cosmos.entities_count(), coords.x, coords.y, rot, vel.x, vel.y)
			+ summary() + cosmos.profiler.sorted_summary(show_profile_details), style(assets::font_id::GUI_FONT, rgba(255, 255, 255, 150)), vec2i(0, 0), 0);

		quick_print(renderer.triangles, multiply_alpha(global_log::format_recent_as_text(assets::font_id::GUI_FONT), 150.f / 255), vec2i(screen_size_i.x - 300, 0), 300);
		quick_print(renderer.triangles, custom_log, vec2i(0, static_cast<int>(bbox.y)), 0);
	}
		
	renderer.call_triangles();
	renderer.clear_triangles();

	triangles.measure(static_cast<double>(renderer.triangles_drawn_total));
	renderer.triangles_drawn_total = 0;

	fps_profiler.end_measurement();
	fps_profiler.new_measurement();
	frame_profiler.end_measurement();
}
Example #11
0
	entity_handle create_character_crosshair(cosmos& world, const vec2i screen_size) {
		auto root = world.create_entity("crosshair");
		auto recoil = world.create_entity("crosshair_recoil_body");
		auto zero_target = world.create_entity("zero_target");

		{
			auto& sprite = root += components::sprite();
			auto& render = root += components::render();
			auto& transform = root += components::transform();
			auto& crosshair = root += components::crosshair();
			auto& processing = root += components::processing();
			
			sprite.set(assets::texture_id::TEST_CROSSHAIR, rgba(255, 255, 255, 255));

			render.layer = render_layer::CROSSHAIR;

			crosshair.base_offset.set(-20, 0);
			crosshair.sensitivity.set(3, 3);
			crosshair.visible_world_area.set(1920, 1080);
			crosshair.update_bounds();

			ingredients::make_always_visible(root);
		}

		{
			auto& force_joint = recoil += components::force_joint();
			zero_target += components::transform();
			components::physics body;
			components::fixtures colliders;

			auto& sprite = recoil += components::sprite();

			sprite.set(assets::texture_id::TEST_CROSSHAIR, rgba(0, 255, 0, 0));

			auto& render = recoil += components::render();
			render.layer = render_layer::OVER_CROSSHAIR;
			ingredients::make_always_visible(recoil);

			auto& info = colliders.new_collider();

			info.shape.from_renderable(recoil);

			info.filter = filters::renderable();
			//info.filter.categoryBits = 0;
			info.density = 0.1f;
			info.sensor = true;

			body.linear_damping = 5;
			body.angular_damping = 5;

			force_joint.chased_entity = zero_target;
			//force_joint.consider_rotation = false;
			//force_joint.distance_when_force_easing_starts = 10.f;
			//force_joint.force_towards_chased_entity = 1000.f;
			//force_joint.power_of_force_easing_multiplier = 1.f;
			force_joint.divide_transform_mode = true;

			recoil += body;
			recoil += colliders;
			recoil.get<components::fixtures>().set_owner_body(recoil);
		}

		root.map_sub_entity(sub_entity_name::CROSSHAIR_RECOIL_BODY, recoil);
		
		root.add_standard_components();
		recoil.add_standard_components();
		zero_target.add_standard_components();

		return root;
	}
std::optional<const_entity_handle> editor_go_to_entity_gui::perform(
	const editor_go_to_settings& settings,
	const cosmos& cosm,
	const vec2 dialog_pos
) {
	if (!show) {
		return std::nullopt;
	}

	using namespace ImGui;
	using namespace augs::imgui;

	{
		const auto max_lines = static_cast<std::size_t>(settings.num_lines);
		const auto left = selected_index / max_lines * max_lines;
		const auto right = std::min(left + max_lines, matches.size());

		const auto size = vec2 {
			static_cast<float>(settings.dialog_width),
			(right - left + 2) * ImGui::GetTextLineHeightWithSpacing()
		};

		set_next_window_rect(
			{
				dialog_pos - vec2(size.x / 2, 0),
				size	
			},
			ImGuiCond_Always
		);
	}

	auto go_to_entity = scoped_window(
		"Go to entity", 
		&show,
	   	ImGuiWindowFlags_NoTitleBar 
		| ImGuiWindowFlags_NoResize 
		| ImGuiWindowFlags_NoScrollbar 
		| ImGuiWindowFlags_NoScrollWithMouse
		| ImGuiWindowFlags_NoMove 
		| ImGuiWindowFlags_NoSavedSettings
	);

	static auto arrow_callback = [](ImGuiTextEditCallbackData* data) {
		auto& input = *reinterpret_cast<text_edit_callback_input*>(data->UserData);
		auto& self = input.self;
		auto& cosm = input.cosm;

		switch (data->EventFlag) {
			case ImGuiInputTextFlags_CallbackHistory: {
				const auto max_index = self.matches.size();

				if (max_index == 0) {
					break;
				}
				
				if (data->EventKey == ImGuiKey_UpArrow) {
					self.moved_since_opening = true;

					if (self.selected_index == 0) {
						self.selected_index = max_index - 1;
					}
					else {
						--self.selected_index; 	
					}
				}
				else if (data->EventKey == ImGuiKey_DownArrow) {
					self.moved_since_opening = true;

					++self.selected_index; 	
					self.selected_index %= max_index;
				}
			}

			break;

			case ImGuiInputTextFlags_CallbackAlways: {
				const auto current_input_text = std::string(data->Buf, data->BufTextLen);
	
				const bool should_rebuild = 
					current_input_text != self.last_input
					|| (current_input_text.empty() && self.matches.empty())
				;
	
				if (!should_rebuild) {
					break;
				}
	
				self.last_input = current_input_text;
	
				self.matches.clear();
				const auto query = current_input_text;
	
				unsigned hits = 0;
					
				cosm.for_each_entity([&](const auto& handle) {
					const auto name = handle.get_name();
	
					if (query.empty() || to_lowercase(name).find(query) != std::string::npos) {
						++hits;
	
						self.matches.push_back(handle.get_id());	
					}
				});	

				if (self.matches.size() > 0) {
					self.selected_index %= self.matches.size();
				}
				else {
					self.selected_index = 0;
				}
			}
			break;
			
			case ImGuiInputTextFlags_CallbackCompletion: {
				if (const auto match = self.get_matching_go_to_entity(cosm)) {
					const auto name = match.get_name();

					if (static_cast<int>(name.length()) < data->BufSize) {
						std::copy(name.c_str(), name.c_str() + name.length() + 1, data->Buf);

						data->BufTextLen = name.length();
						data->CursorPos = name.length();
						data->BufDirty = true;
					}
				}

				break;
			}

			default: break;
		}

		return 0;
	};

	text("Go to entity");
	ImGui::SameLine();
	
	bool was_acquired = false;
	
	if (ImGui::GetCurrentWindow()->GetID("##GoToEntityInput") != GImGui->ActiveId) {
		ImGui::SetKeyboardFocusHere();
		was_acquired = true;
	}
	
	text_edit_callback_input input {
		cosm,
		*this
	};

	auto scope = augs::scope_guard([&](){
		if (!was_acquired && ImGui::GetCurrentWindow()->GetID("##GoToEntityInput") != GImGui->ActiveId) {
			show = false;
		}
	});

	if (input_text<256>(
		"##GoToEntityInput", 
		textbox_data,
			ImGuiInputTextFlags_CallbackHistory 
			| ImGuiInputTextFlags_EnterReturnsTrue 
			| ImGuiInputTextFlags_CallbackAlways
			| ImGuiInputTextFlags_CallbackCompletion
		, 
		arrow_callback,
		reinterpret_cast<void*>(&input)
	)) {
		return get_matching_go_to_entity(cosm);
	}

	{
		const auto query = last_input;

		const auto max_lines = static_cast<std::size_t>(settings.num_lines);
		const auto left = selected_index / max_lines * max_lines;
		const auto right = std::min(left + max_lines, matches.size());

		for (std::size_t i = left; i < right; ++i) {
			if (i == selected_index) {
				bool s = true;
				ImGui::PushID(static_cast<int>(i));
				ImGui::Selectable("##gotoentity", &s);
				ImGui::PopID();
				ImGui::SameLine(0.f, 0.f);
			}

			const auto m = matches[i];
			const auto name = cosm[m].get_name();
			const auto matched_name = to_lowercase(name);

			const auto unmatched_left = matched_name.find(query);
			const auto unmatched_right = unmatched_left + query.length();

			text(name.substr(0, unmatched_left));
			ImGui::SameLine(0.f, 0.f);

			text_color(name.substr(unmatched_left, unmatched_right - unmatched_left), green);
			ImGui::SameLine(0.f, 0.f);

			text(name.substr(unmatched_right));

			ImGui::SameLine(settings.dialog_width * 0.7);

			text("(id: %x)", m.raw);
		}
	}

	return std::nullopt;
}
Example #13
0
void particles_simulation_system::advance_visible_streams_and_all_particles(camera_cone cone, const cosmos& cosmos, const augs::delta delta, interpolation_system& interp) {
	for (auto& particle_layer : particles) {
		for (auto& p : particle_layer) {
			p.integrate(delta.in_seconds());
		}

		erase_remove(particle_layer, [](const resources::particle& a) { return a.lifetime_ms >= a.max_lifetime_ms; });
	}

	cone.visible_world_area *= 2.5f;

	const auto targets = 
		cosmos[cosmos.systems_temporary.get<dynamic_tree_system>().determine_visible_entities_from_camera(cone, components::dynamic_tree_node::tree_type::PARTICLE_EXISTENCES)];

	for (const auto it : targets) {
		auto& cache = get_cache(it);
		const auto& existence = it.get<components::particles_existence>();

		const bool should_rebuild_cache = cache.recorded_existence != existence;

		if (should_rebuild_cache) {
			randomization rng = existence.rng_seed;
			cache.recorded_existence = existence;
			cache.emission_instances.clear();

			for (auto emission : (*existence.input.effect)) {
				emission.apply_modifier(existence.input.modifier);

				cache.emission_instances.push_back(emission_instance());
				auto& target_stream = *cache.emission_instances.rbegin();

				const auto var_v = rng.randval(emission.base_velocity_variation);
				//LOG("V: %x", var_v);
				target_stream.velocity.set(std::max(0.f, emission.base_velocity.first - var_v / 2), emission.base_velocity.second + var_v / 2);
				//LOG("Vl: %x Vu: %x", target_stream.velocity.first, target_stream.velocity.second);

				target_stream.stream_info = emission;
				target_stream.enable_streaming = true;
				target_stream.stream_lifetime_ms = 0.f;
				target_stream.angular_offset = rng.randval(emission.angular_offset);
				target_stream.target_spread = rng.randval(emission.spread_degrees);
				target_stream.target_particles_per_sec = rng.randval(emission.particles_per_sec);
				target_stream.swing_spread = rng.randval(emission.swing_spread);
				target_stream.swings_per_sec = rng.randval(emission.swings_per_sec);

				target_stream.min_swing_spread = rng.randval(emission.min_swing_spread);
				target_stream.min_swings_per_sec = rng.randval(emission.min_swings_per_sec);
				target_stream.max_swing_spread = rng.randval(emission.max_swing_spread);
				target_stream.max_swings_per_sec = rng.randval(emission.max_swings_per_sec);

				target_stream.stream_max_lifetime_ms = rng.randval(emission.stream_duration_ms);
				target_stream.stream_particles_to_spawn = rng.randval(emission.num_of_particles_to_spawn_initially);
				target_stream.swing_speed_change = rng.randval(emission.swing_speed_change_rate);
				target_stream.swing_spread_change = rng.randval(emission.swing_spread_change_rate);

				target_stream.fade_when_ms_remaining = rng.randval(emission.fade_when_ms_remaining);
			}
		}

		const auto& transform = it.viewing_transform(interp) + existence.current_displacement;
		randomization rng = cosmos.get_rng_seed_for(it) + cosmos.get_total_steps_passed();

		bool should_destroy = true;

		for (auto& instance : cache.emission_instances) {
			const float stream_delta = std::min(delta.in_milliseconds(), instance.stream_max_lifetime_ms - instance.stream_lifetime_ms);

			instance.stream_lifetime_ms += stream_delta;

			if (instance.stream_lifetime_ms > instance.stream_max_lifetime_ms) {
				continue;
			}

			auto new_particles_to_spawn_by_time = instance.target_particles_per_sec * (stream_delta / 1000.f);

			instance.stream_particles_to_spawn += new_particles_to_spawn_by_time;

			instance.swings_per_sec += rng.randval(-instance.swing_speed_change, instance.swing_speed_change);
			instance.swing_spread += rng.randval(-instance.swing_spread_change, instance.swing_spread_change);

			if (instance.max_swing_spread > 0) {
				augs::clamp(instance.swing_spread, instance.min_swing_spread, instance.max_swing_spread);
			}
			if (instance.max_swings_per_sec > 0) {
				augs::clamp(instance.swings_per_sec, instance.min_swings_per_sec, instance.max_swings_per_sec);
			}

			const int to_spawn = static_cast<int>(std::floor(instance.stream_particles_to_spawn));

			const auto segment_length = existence.distribute_within_segment_of_length;
			const vec2 segment_A = transform.pos + vec2().set_from_degrees(transform.rotation + 90).set_length(segment_length / 2);
			const vec2 segment_B = transform.pos - vec2().set_from_degrees(transform.rotation + 90).set_length(segment_length / 2);

			for (int i = 0; i < to_spawn; ++i) {
				const float t = (static_cast<float>(i) / to_spawn);
				const float time_elapsed = (1.f - t) * delta.in_seconds();

				const vec2 segment_position = augs::interp(segment_A, segment_B, rng.randval(0.f, 1.f));

				spawn_particle(rng, instance, segment_position, transform.rotation +
					instance.swing_spread * static_cast<float>(sin((instance.stream_lifetime_ms / 1000.f) * 2 * PI_f * instance.swings_per_sec))
					, instance.target_spread, instance.stream_info).integrate(time_elapsed);

				instance.stream_particles_to_spawn -= 1.f;
			}
		}
	}
}