书接上回,这一章我们介绍相机抖动、粒子特效、高亮材质等功能实现。
一、最终效果
二、相机抖动
游戏玩法完成后,让我们看看是否能让游戏的展示变得更加有趣。当球击中某物时,让摄像头抖动是一个让玩家感受到强烈冲击的简单方法。这种方式模拟了玩家感受到球击中场地边缘的感觉。为了进一步增强沉浸感,我们从俯视视角切换到透视视角。将摄像头的位置设置为(0,20,-19),并将其X轴旋转角度设置为50。
2.1推挤
要控制相机的行为,可以创建一个名为LivelyCamera的脚本。这个相机可以在XZ平面上通过给定的冲力来移动,或者在Y维度上给予一定的抖动。这是通过给相机一个3D速度来实现的,这个速度在LateUpdate时应用,也就是在一帧的所有抖动和推动操作完成之后。
抖动是通过一个公共的JostleY方法实现的,该方法通过可配置的力量来增加Y轴的速度,默认设置为40。推动则是通过公共的PushXZ方法完成的,该方法接受一个2D冲力参数,并将其添加到速度中,然后通过可配置的推动力量因子进行缩放,默认设置为1。
using UnityEngine; public class LivelyCamera : MonoBehaviour { [SerializeField, Min(0f)] float jostleStrength = 40f, pushtrength = 1f; Vector3 velocity; public void JostleY () => velocity.y += jostleStrength; public void PushXZ (Vector2 impulse) { velocity.x += pushStrength * impulse.x; velocity.z += pushStrength * impulse.y; } void LateUpdate () { transform.localPosition += velocity * Time.deltaTime; } }
将此组件添加到主相机上,然后为游戏添加一个配置字段,并将其与相机连接起来。
[SerializeField] LivelyCamera livelyCamera;
在 BounceXIfNeeded 中检测到反弹时,在执行反弹之前,使用球的速度作为冲量来调用 PushXZ。
void BounceXIfNeeded (float x) { float xExtents = arenaExtents.x - ball.Extents; if (x < -xExtents) { livelyCamera.PushXZ(ball.Velocity); ball.BounceX(-xExtents); } else if (x > xExtents) { livelyCamera.PushXZ(ball.Velocity); ball.BounceX(xExtents); } }
在执行Y轴反弹之前,也在 BounceY 中做相同的操作。另外,当得分时使相机产生抖动。
void BounceY (float boundary, Paddle defender, Paddle attacker) { float durationAfterBounce = (ball.Position.y - boundary) / ball.Velocity.y; float bounceX = ball.Position.x - ball.Velocity.x * durationAfterBounce; BounceXIfNeeded(bounceX); bounceX = ball.Position.x - ball.Velocity.x * durationAfterBounce; livelyCamera.PushXZ(ball.Velocity); ball.BounceY(boundary); if (defender.HitBall(bounceX, ball.Extents, out float hitFactor)) { ball.SetXPositionAndSpeed(bounceX, hitFactor, durationAfterBounce); } else { livelyCamera.JostleY(); if (attacker.ScorePoint(pointsToWin)) { EndGame(); } } }
2.2弹簧和阻尼
现在相机会被推动和晃动,但它的速度会持续,因此竞技场很快就会移出视野。为了将 LivelyCamera 快速恢复原位,我们使用一个简单的弹簧机制将其锚定到其初始位置。为其设置一个可配置的弹簧强度,默认设置为100,以及一个阻尼强度,默认设置为10。另外,给它一个锚定位置,并将其设置为相机唤醒时的位置。
[SerializeField, Min(0f)] float springStrength = 100f, dampingStrength = 10f, jostleStrength = 40f, pushStrength = 1f; Vector3 anchorPosition, velocity; void Awake () => anchorPosition = transform.localPosition;
我们通过使用相机当前位移乘以弹簧强度来计算加速度,来实现弹簧效果。同时,我们还通过负加速度(等于当前速度乘以阻尼强度)来减慢移动速度。
void LateUpdate () { Vector3 displacement = anchorPosition - transform.localPosition; Vector3 acceleration = springStrength * displacement - dampingStrength * velocity; velocity += acceleration * Time.deltaTime; transform.localPosition += velocity * Time.deltaTime; }
2.3最大增量时间
我们的简单弹簧规则只有在帧率足够高时才能表现良好。它抵抗推动和碰撞,将相机拉回到其锚点,但在静止之前可能会造成一些过度移动和轻微的晃动。然而,如果帧率太低,过度移动可能会夸大其动量,导致失控,加速而不是减速。这个问题可以通过强制使用非常低的帧率来演示,通过在唤醒方法中添加Application.targetFrameRate = 5;来实现。之后你需要将其设置回零以移除限制,因为此设置是持久的。
当帧率足够高时,这个问题不会出现。因此,我们可以通过强制一个小的时间增量来避免它。我们可以通过使用FixedUpdate来移动相机来实现这一点。但是,因为这会强制一个精确的时间增量,这会导致微卡顿,因为相机可能不会在每帧中都以相同的次数进行更新,这是非常明显的,因为它会影响整个视图的运动。此外,它还限制了相机运动的有效帧率。
一个简单的解决方案是强制一个最大的时间增量,而不是最小的。在LivelyCamera中添加一个可配置的最大值,默认设置为六十分之一秒。然后将LateUpdate中的代码移到一个新的TimeStep方法中,该方法以时间增量作为参数。让LateUpdate在当前帧的增量范围内尽可能多次地调用TimeStep,然后用剩余的增量再调用一次。
[SerializeField, Min(0f)] float springStrength = 100f, dampingStrength = 10f, jostleStrength = 40f, pushStrength = 1f, maxDeltaTime = 1f / 60f; … void LateUpdate () { float dt = Time.deltaTime; while (dt > maxDeltaTime) { TimeStep(maxDeltaTime); dt -= maxDeltaTime; } TimeStep(dt); } void TimeStep (float dt) { Vector3 displacement = anchorPosition - transform.localPosition; Vector3 acceleration = springStrength * displacement - dampingStrength * velocity; velocity += acceleration * dt; transform.localPosition += velocity * dt; }
三、视觉效果
最后一步是稍微改善我们游戏的视觉效果。我们将采用简单的发光霓虹灯风格。
首先,通过简单地关闭方向光来使所有东西变暗。同时,将相机的环境设置为纯色背景。然后启用相机的渲染/后期处理选项,并将其渲染/抗锯齿设置为FXAA。
尽管在黑色背景下环境光看起来不太合理,但仍保留它,以确保竞技场仍然有一定的可见性。
为什么使用 FXAA 而不是 MSAA?
我们将使用高强度 HDR 颜色。URP 的 FXAA 可以处理这些颜色,但其 MSAA 不能。
3.1发光球体
球将是我们唯一的光源。给它添加一个子点光源,将其颜色设置为黄色,亮度设置为20,范围设置为100。
球应该发光。为它创建一个无光照的着色器图,并包含一个HDR颜色属性。使用这个着色器图创建一个高强度黄色的材质,并将其分配给球的预制件。
为了让它看起来发光,我们需要应用一个发光后处理效果。通过GameObject / Volume / Global Volume创建一个全局体积。选择它,并为其创建一个新的体积配置文件。添加一个Bloom,将强度设置为1并启用High Quality Filtering。另外,添加一个Tonemapping,并将其设置为ACES。
调整URP资源,以便其“Post-processing / Grading Mode”设置为HDR。我们还可以将“Post-processing / Volume Update Mode设置为Via Scripting。这可以防止Unity在每一帧中不必要地更新体积数据,因为我们永远不会改变它。
3.2弹跳粒子
现在,这个球看起来像一个高能量的立方体。为了强化这个概念,当球体反弹时,我们可以让火花出现。创建一个未点亮的着色器图,它使用顶点颜色与强度属性相乘。将其表面类型设置为透明,并将混合模式设置为加性,这样它总是会更亮。为这个球体创建一个粒子材质,并将其强度设置为10。
在Unity中创建一个粒子系统游戏对象并将其放置在原点。禁用“Looping”和“Play On Awake”。设置“Start Lifetime”为常数范围0.5–1,“Start Speed”为2–4,以及“Start Size”为0.5。将“Simulation Space”更改为“World”,并将“Emitter Velocity Mode”更改为“Transform”。
将发射模块的“Rate”和“Burst”都设置为零。
形状模块默认设置为锥体,但将其“Angle”设置为45,并将其“Radius”设置为0.5。
启用“Color over Lifetime”模块,使用从黄色到红色的颜色渐变,并在两端将其透明度设置为零,在10%处设置为255。
启用“Size over Lifetime”模块,使用从一到零的线性曲线。
将渲染器模块的“Mode”设置为“Mesh”,并使用立方体作为默认网格。为其指定我们的粒子材质。
这个粒子系统不是球本身的一部分,但球需要一个可配置的引用来生成粒子,以及一个配置选项来控制每次反弹时生成多少粒子,默认设置为20。
[SerializeField] ParticleSystem bounceParticleSystem; [SerializeField] int bounceParticleEmission = 20;
创建一个方法,用于处理反弹粒子的发射,通过调用反弹粒子系统的Emit方法来实现。发射锥体必须被正确地定位和旋转,因此该方法需要接收X和Z位置以及Y旋转作为参数。使用这些参数来调整系统的形状模块。
void EmitBounceParticles (float x, float z, float rotation) { ParticleSystem.ShapeModule shape = bounceParticleSystem.shape; shape.position = new Vector3(x, 0f, z); shape.rotation = new Vector3(0f, rotation, 0f); bounceParticleSystem.Emit(bounceParticleEmission); }
在BounceX和BounceY方法中调用该方法并传入适当的参数。边界是对应维度上的位置。第二个位置可以通过回溯球的位置到反弹发生的那一刻来找到。旋转取决于反弹的维度以及边界是负数还是正数。
public void BounceX (float boundary) { float durationAfterBounce = (position.x - boundary) / velocity.x; position.x = 2f * boundary - position.x; velocity.x = -velocity.x; EmitBounceParticles( boundary, position.y - velocity.y * durationAfterBounce, boundary < 0f ? 90f : 270f ); } public void BounceY (float boundary) { float durationAfterBounce = (position.y - boundary) / velocity.y; position.y = 2f * boundary - position.y; velocity.y = -velocity.y; EmitBounceParticles( position.x - velocity.x * durationAfterBounce, boundary, boundary < 0f ? 0f : 180f ); }
3.3启动粒子
在游戏开始时,当球出现时,我们也让火花四溅吧。将现有的粒子系统转换为预制件,并在原点位置放置第二个实例作为新的起始粒子系统。增加其“Start Lifetime”到0.5到1.5秒,使其持续时间稍长一些,并将其“Shape”设置为球形。
在“Sphere”的组件中添加一个配置字段用于这个粒子系统,同时添加一个字段用于设置游戏开始时产生的粒子数量,默认设置为100。当开始新游戏时,发射这些粒子。
[SerializeField] ParticleSystem bounceParticleSystem, startParticleSystem; [SerializeField] int bounceParticleEmission = 20, startParticleEmission = 100; … public void StartNewGame () { … startParticleSystem.Emit(startParticleEmission); }
3.4粒子拖尾
第三个也是最后一个粒子效果将是球的轨迹。再次创建粒子系统预制件的实例,这次启用“Looping”和“Play On Awake”。将其“Start Lifetime”设置为1到1.25秒,并将“Start Speed”设置为零。将其“Shape”更改为从体积中发射的盒子形状。为了让它在移动时发射粒子,请将“Emit / Rate over Distance”设置为2。
同样,在“Sphere”的组件中添加一个配置字段用于这个系统,并在其可视化更新时与球的位置同步。我们不将轨迹系统设置为球的子对象,以便在游戏结束时球被停用后轨迹仍然可见,否则轨迹会立即消失。
[SerializeField] ParticleSystem bounceParticleSystem, startParticleSystem, trailParticleSystem; … public void UpdateVisualization () => trailParticleSystem.transform.localPosition = transform.localPosition = new Vector3(position.x, 0f, position.y);
这个方法有效,但轨迹效果会在每局游戏结束时和开始时随着球的传送而移动。我们可以通过两种方法避免这种情况。首先,我们需要在游戏结束时关闭发射,并在新游戏开始时打开它。这是通过设置发射模块的“enabled”属性来实现的,所以让我们为此添加一个方便的方法。
public void StartNewGame () { … SetTrailEmission(true); } public void EndGame () { … SetTrailEmission(false); } … void SetTrailEmission (bool enabled) { ParticleSystem.EmissionModule emission = trailParticleSystem.emission; emission.enabled = enabled; }
第二,粒子系统会记住其旧位置。为了清除它以避免在新游戏开始时显示传送轨迹,我们需要在它上面调用“Play”。
SetTrailEmission(true); trailParticleSystem.Play();
这意味着我们可以将“Play On Awake”重新设置为禁用,因为我们现在明确地在需要时播放它。
3.5反应表面
球和它的粒子不是唯一会发光的东西。让我们让它成为表面也可以对被暂时发光的撞击做出反应。创建一个带有BaseColor、HDR_EmissionColor和默认为-1000的TimeOfLastHit属性的发光着色器图。
它的发射颜色取决于最后一次击中发生的时间。它将在击中的那一刻全力以赴,并将在下一秒线性淡出。这可以通过从当前时间减去最后一次击中的时间,从1中减去,使其饱和,并使用它来缩放发射颜色来完成。
用这个着色器图创建一个材质,并将其用于桨预制件。使用白色作为底色,使用高强度白色作为发射色。
当Paddle唤醒时检索一个材料实例,并在HitBall成功时更新其最后一次击中的时间。这将使球拍在设法击中球时发光。
static readonly int timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit"); … Material paddleMaterial; void Awake () { paddleMaterial = GetComponent<MeshRenderer>().material; SetScore(0); } … public bool HitBall (float ballX, float ballExtents, out float hitFactor) { … bool success = -1f <= hitFactor && hitFactor <= 1f; if (success) { paddleMaterial.SetFloat(timeOfLastHitId, Time.time); } return success; }
让我们更进一步,当得分时,也让作为对手球门的竞技场边界发光。创建另一个反应表面材料,其颜色设置为中灰色,并将其用于竞技场边界预制件。然后给Paddle
一个对其目标的MeshRenderer
的可配置引用,以及一个可配置的HDR目标颜色。
当桨唤醒时检索其目标材料的实例,并将材料的发射颜色设置为目标颜色。当得分时设置其最后击中的时间。
static readonly int emissionColorId = Shader.PropertyToID("_EmissionColor"), timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit"); [SerializeField] TextMeshPro scoreText; [SerializeField] MeshRenderer goalRenderer; [SerializeField, ColorUsage(true, true)] Color goalColor = Color.white; … Material goalMaterial, paddleMaterial; void Awake () { goalMaterial = goalRenderer.material; goalMaterial.SetColor(emissionColorId, goalColor); paddleMaterial = GetComponent<MeshRenderer>().material; SetScore(0); } … public bool ScorePoint (int pointsToWin) { goalMaterial.SetFloat(timeOfLastHitId, Time.time); SetScore(score + 1, pointsToWin); return score >= pointsToWin; }
将球拍连接到适当的渲染。底部玩家颜色使用高强度绿色,顶部AI颜色使用高强度红色。
3.6彩色文本
我们还通过着色文本并使其发光来结束。首先将文本预制件的默认字体材质颜色设置为高强度黄色。
我们将使用得分显示的目标颜色,但有一个转折。我们将从零开始黑色,所以分数最初在我们的黑色背景上是不可见的。一旦得分颜色等于获胜的分数,它们将达到最大强度。
在这种情况下,通过文本的fontMaterial
属性检索材质实例,其面部颜色着色器属性名为__FaceColor_。
static readonly int emissionColorId = Shader.PropertyToID("_EmissionColor"), faceColorId = Shader.PropertyToID("_FaceColor"), timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit"); … Material goalMaterial, paddleMaterial, scoreMaterial; void Awake () { goalMaterial = goalRenderer.material; goalMaterial.SetColor(emissionColorId, goalColor); paddleMaterial = GetComponent<MeshRenderer>().material; scoreMaterial = scoreText.fontMaterial; SetScore(0); } … void SetScore (int newScore, float pointsToWin = 1000f) { score = newScore; scoreText.SetText("{0}", newScore); scoreMaterial.SetColor(faceColorId, goalColor * (newScore / pointsToWin)); SetExtents(Mathf.Lerp(maxExtents, minExtents, newScore / (pointsToWin - 1f))); }
这就是教程对这个原型的介绍,尽管你可以更进一步。请注意,一旦你发现了人工智能的弱点,默认配置就很容易击败它。这对开发很方便,但应该对其进行调整以提供所需的挑战级别。
工程文件链接:https://download.csdn.net/download/m0_50811529/89446520
标签:ball,游戏,乒乓,void,float,原型,设置,velocity,boundary From: https://blog.csdn.net/m0_50811529/article/details/139300020