首页 > 其他分享 >游戏原型系列:(1)乒乓广场——续

游戏原型系列:(1)乒乓广场——续

时间:2024-06-17 14:32:08浏览次数:17  
标签:ball 游戏 乒乓 void float 原型 设置 velocity boundary

书接上回,这一章我们介绍相机抖动、粒子特效、高亮材质等功能实现。

一、最终效果

二、相机抖动

游戏玩法完成后,让我们看看是否能让游戏的展示变得更加有趣。当球击中某物时,让摄像头抖动是一个让玩家感受到强烈冲击的简单方法。这种方式模拟了玩家感受到球击中场地边缘的感觉。为了进一步增强沉浸感,我们从俯视视角切换到透视视角。将摄像头的位置设置为(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);
	}

BounceXBounceY方法中调用该方法并传入适当的参数。边界是对应维度上的位置。第二个位置可以通过回溯球的位置到反弹发生的那一刻来找到。旋转取决于反弹的维度以及边界是负数还是正数。

	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反应表面

球和它的粒子不是唯一会发光的东西。让我们让它成为表面也可以对被暂时发光的撞击做出反应。创建一个带有BaseColorHDR_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

相关文章

  • JavaScript妙笔生花:打造沉浸式中国象棋游戏体验
    前言随着信息技术的飞速发展,Web开发领域也出现了翻天覆地的变化。JavaScript作为前端开发中不可或缺的编程语言,其重要性不言而喻。而当我们谈论到利用JavaScript打造一款沉浸式的中国象棋游戏体验时,我们不仅仅是在开发一个游戏,更是在进行一种文化的传承和创新。以下将探讨......
  • 俄罗斯方块小游戏(附源码)
    游戏展示一.导包importturtleimportrandom二.定义一个Block类定义一个Block类,用于表示游戏中的方块,包含颜色和形状。classBlock:def__init__(self,color,tiles):self.color=colorself.tiles=tiles三.定义了7个不同的Block对象定......
  • 雷电模拟器改真机保姆级教程,游戏搬砖党必备!
    游戏工作室今天分享最新防封电脑模拟器改真机技术,适用于所有模拟器搬砖游戏,有效防止电脑模拟器封禁问题。游戏搬砖玩家可以像使用真机一样流畅地操作游戏,电脑模拟器可以模拟真机的运行环境,让游戏服务器难以察觉到运行设备的差异,从而起到防封的作用更加安全。重要提示:要多开模拟......
  • 在Linux中,什么是运维?什么是游戏运维?
    在Linux中,运维和游戏运维是两种不同的运维角色,但它们都涉及到对系统、网络和应用程序的管理、维护和优化。以下是对它们的详细解释:一、运维运维(OperationandMaintenance),通常指互联网运维,是技术部门中的一个重要组成部分,与研发、测试、系统管理共同构成互联网产品技术支撑的四......
  • 0055-跳跃游戏
    55.跳跃游戏给你一个非负整数数组nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以,返回true;否则,返回false。示例1:输入:nums=[2,3,1,1,4]输出:true解释:可以先跳1步,从下标0到达下标......
  • python做的游戏有哪些
    比较大型的,使用Python的游戏有两个,一个是《EVE》,还有一个是《文明》。另外GitHub上有很多开源的小游戏,下面给大家介绍一下:1.Github上面有个项目FreePythonGames,里面集合了不少的Python开发的小游戏,能玩,也适合新手用来练练手,另外PyGame这个网站里面里面集合了很多Python......
  • 【华为OD】D卷真题200分:数字游戏 python代码实现[思路+代码]
    【华为OD】2024年C、D卷真题集:最新的真题集题库C/C++/Java/python/JavaScript【华为OD】2024年C、D卷真题集:最新的真题集题库C/C++/Java/python/JavaScript-CSDN博客JS、Java、C、python、C++代码实现:【华为OD】D卷真题200分:数字游戏JavaScript代码实现[思路+代码]-CSDN......
  • 趣味C语言——【猜数字】小游戏
    ......
  • 游戏缓存与异步持久化的完美邂逅
    1、问题提出游戏服务器,需要频繁的读取玩家数据,同时也需求频发修改玩家数据,并持久化到数据库。为了提高游戏服务器的性能,我们应该怎么处理呢?2、应用程序缓存缓存,是指应用程序从数据库读取完数据之后,就将数据缓存在进程内存或第三方内存(例如redis)。游戏服务器对于玩家数据的读......
  • JavaScript 的原型链机制
    JavaScript的原型链机制是其继承模型的核心概念,它允许对象通过原型链访问和继承其他对象的属性和方法。原型链机制是实现JavaScript面向对象编程的基础。1.原型和原型链的基本概念原型对象(prototype):每个JavaScript对象(除了null)都有一个与之关联的对象,这个对象就......