FPS项目声音设计框架

Sound Framework Of A FPS Project

Posted by 李AA on August 12, 2019

前言

  • 此项目是一个支持32人同场竞技的FPS团队竞赛类游戏。音频部分工作重点是枪声和声学空间塑造,力求突出音效对各种空间参数的敏感与真实性。

  • AudioFramework

武器

  • 资源层级
    • 声音对象距离(close, mid, far)
      • 第一/第三人称(FP, TP)
        • 室内/室外(indoor, outdoor)
          • 武器状态(fire, reload, stop…)
            • 具体武器种类(AKM, M249, M4…)
  • 击中声
    • 材质分类
  • 其他武器

声音对象距离

  • 用引擎端传来的声音对象间距离来做枪的第一层级,三个距离区间不同素材,不同空间化设置。

第一/第三人称

  • 引擎端判断对象人称属性,切换FP/TP层级,两套资源

室内/室外

  • 室内室外某些状态的资源有区别,这里室内外我们用了自定义的Spatial Volume来检测,详见空间

武器状态和具体武器种类

  • 所有武器相关模块都用自定义AkComponent,武器的特别需求有第一/第三人称区别,室内室外区别,连发枪第一发和后面发资源不同,有弹壳掉落声,连发枪停止射击时不能马上中断,需要播完整资源。

  • 状态模块选择

    • 通过一组枚举表来管理复杂状态量

  • 状态切换
    • 状态切换主要是判断第一次按下鼠标键的时间,然后触发连发枪第一枪模块和总的状态切换模块。最后PostEvent还有一个回调用来触发连发枪尾音模块

  • 连发枪第一发模块
    • 通过上个模块判断的按下鼠标的时长来判断是点射还是连发

  • 连发枪尾音模块
    • 因为Loop资源的特殊性,连发枪在通知上也是只有两次(开始/停止),所以只能通过增加一个射击声尾巴资源来增加真实性,这里主要是判断是否是连射结束,然后播放资源,资源提前打上Marker标记

  • 子弹掉落声模块
    • 如果是连发枪,因为只有两次通知(开始/停止),所以需要自己模拟掉落Loop,通过一张Map来维护Loop时间间隔。

单事件设计

  • 枪部分是整个项目资源量最大,层级最深的部分。我们使用了单事件的模式,只暴露了一个Play_Guns事件给引擎端,所有层级切换使用switch的配合完成。这样的设计优点是减少事件的数量便于在引擎端的管理,缺点是调用层级复杂,需要良好的switch管理。

  • 对于这种单事件的设计,需要注意的是每个事件实例同一时间只播放一个声音资源,若声音间有重叠的需要(比如射击的回响声和换弹夹的声音可能同时出现),则需要多个事件实例,在引擎端表现为多个声音组件。

人物

  • 人物声音模块因为项目问题,拆分为动画相关的和控制相关,所有声音播放还是通过挂在人物身上自定义AkComponent。

  • 人物

    • 移动声音
      • 具体移动状态(Walk, Run, Jump…)
    • 背包声音
      • 具体移动状态(Walk, Run, Jump…)
    • 衣服声音
      • 具体移动状态(Walk, Run, Jump…)

动画关联

  • 和动画关联的声音通过自定义AnimNotify的方式来触发。

  • 编辑器声音播放模块

  • Gameplay声音模块

  • 无效数据检查模块

  • 还有一部分中间动作状态的通过动作状态机上的Event来出发

控制关联

  • 人物走路,跑步,跳跃等一系列动作可以通过绑定MovementComponent通过移动时的参数来调用,素材需要调整为loop

材质区分

  • 通过一组接口来进行材质的区分与相应资源的切换,这里方案用的是LineTrace,具体测试时在楼梯等镂空地方LineTrace会有检测失误,所以在有材质镂空的地方需要再进行标记

空间

  • 空间上,因为我们需求是单一Box同时满足TriggerBox和Spatial Audio Volume的功能。但是测试中发现AVolume类在碰撞检测时会有阻塞,在SetSwitch时会有较大卡壳。反之AkLateReverbComponent和AkRoomComponent组件只能挂在AVolume类对象上,TriggerBox的父类是AActor不满足要求,所以进行了AkReverbComponent和AkRoomComponent的重定义,使其满足可以挂在TriggerBox类上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//之所以只能挂在AVolume类上是因为依赖了AVolume的这个功能
bool UAkLateReverbComponent::HasEffectOnLocation(const FVector& Location) const
{
	// Need to add a small radius, because on the Mac, EncompassesPoint returns false if
	// Location is exactly equal to the Volume's location
	static float RADIUS = 0.01f;
	return LateReverbIsActive() && ParentVolume->EncompassesPoint(Location, RADIUS);
}

//这个是改写了可以支持TriggerBox的EncompassesPoint功能
bool UAkLateReverbComponent::EncompassesPoint(FVector Point, float SphereRadius, float* OutDistanceToPoint) const
{
	auto shapeComp = TriggerActor->GetCollisionComponent();

	if (nullptr != shapeComp)
	{
		FVector ClosestPoint;
		float DistanceSqr;
		if (false == shapeComp->GetSquaredDistanceToCollision(Point, DistanceSqr, ClosestPoint))
		{
			if (OutDistanceToPoint)
			{
				*OutDistanceToPoint = -1.f;
			}
		}

		if (OutDistanceToPoint)
		{
			*OutDistanceToPoint = FMath::Sqrt(DistanceSqr);
		}

		return DistanceSqr >= 0.f && DistanceSqr <= FMath::Square(SphereRadius);
	}
	
	else
	{
		//UE_LOG(AkLateReverbComponent, Log, TEXT("AkLateReverbComponent::EncompassesPoint : No TriggerActor"));
		return false;
	}
}

//然后再修改几个对Parent对象类型进行检查的地方就可以了
void UAkLateReverbComponent::InitializeParentVolume()
{
	ParentVolume = Cast<AVolume>(GetOwner());
	//这里
  if (!ParentVolume)
	{
		bEnable = false;
		UE_LOG(LogAkAudio, Error, TEXT("UAkLateReverbComponent requires to be attached to an actor inheriting from AVolume."));
	}
}

UI

  • UI资源创作因为是多人协作,而且存在后期修改可能,所以选用了用DataTable获取数据的方式

  • 在Widget中用这个接口来配置,之后有任何修改,只需要在DataTable中进行

环境声

  • 使用单例SoundManager来管理所有2D类资源播放

单元测试用例

  • 游戏需要运行在专用服务器上,而且需要固定人数才能匹配,所以开发环境下需要一个可以模拟第三人称的测试用例,测试用例要可以播放人物身上的所有声音已经可以移动。

  • 控制模块

  • 事件模块

总结

  • 整个项目工作量主要集中在枪声和空间组件的设计上,对于单事件的设计在后期需求变化增多时也出现了缺陷,需要用补充事件来进行完善。地图内因为存在大量建筑,所以就有大量的空间组件,空间组件的优化也成了最重要一部分,这个我会另写文章讨论。