当前位置: 首页 > news >正文

国内比较好的软文网站来个网站急急急2021年

国内比较好的软文网站,来个网站急急急2021年,网络优化的基本流程,石家庄长安区网站建设公司今天我来拆解一个3D赛车游戏的Demo#xff0c;看看实现一个赛车游戏需要哪些内容。 主要的技术难点包括#xff1a; 如何实现无限的赛道#xff1f; 如何基于柏林噪声实现地形#xff1f; 如何创造圆柱形的赛道#xff1f; 如何实现道具效果#xff1f; 首先各种游…今天我来拆解一个3D赛车游戏的Demo看看实现一个赛车游戏需要哪些内容。 主要的技术难点包括 如何实现无限的赛道 如何基于柏林噪声实现地形 如何创造圆柱形的赛道 如何实现道具效果 首先各种游戏内物体的素材就靠各位自己去找了我的素材如图所示 这是一个简单的四轮小车可以看到比较特殊的就是这是多个碰撞体的结合一个立方体碰撞体加上四个轮毂碰撞体(用胶囊碰撞体实现)。 除此之外我们还有作为路障的物体以及用于加分的物体 我们的设计很简单小车遇到路障或者门的门框而不是通过的话就散架是的散架否则如果穿过门就加分。 那么首先我们有一个路障的代码 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Obstacle : MonoBehaviour {// 游戏管理器的引用GameManager manager;void Start(){// 查找场景中的游戏管理器manager GameObject.FindObjectOfTypeGameManager();// 确保障碍物拥有Obstacle标签if (!gameObject.CompareTag(Obstacle)){gameObject.tag Obstacle;Debug.Log($[Obstacle] 障碍物标签已设置为Obstacle: {gameObject.name});}}// 碰撞逻辑已移除由Car.cs负责处理所有碰撞 } 我们初始化游戏管理器然后强迫挂载Obstacle的脚本的Tag修改为Obstacle。 门也得挂载这个脚本但同时门还要负责加分所以还有另一个脚本。 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Gate : MonoBehaviour {// 在 Inspector 中可见的变量用于音效播放public AudioSource scoreAudio; // 得分时播放的音效// 在 Inspector 中不可见的变量GameManager manager; // 游戏管理器引用bool addedScore; // 标记是否已经得分void Start(){// 查找游戏管理器manager GameObject.FindObjectOfTypeGameManager();}void OnTriggerEnter(Collider other){// 检查玩家是否通过了这个关卡门并且还没有得分if(!other.gameObject.transform.root.CompareTag(Player) || addedScore)return; // 如果碰撞物体不是玩家或已经得分则不执行// 增加分数并播放音效addedScore true; // 标记已得分manager.UpdateScore(1); // 更新分数scoreAudio.Play(); // 播放得分音效} } 大体的内容注释也写得很清楚了这个门首先获取到GameManager和AudioSource的实例然后使用Unity自带的OnTriggerEnter函数在标签为Player的物体且这个门还没有执行过加分发生碰撞检测时执行加分。 然后是我们的一些道具 首先我们有一个总的道具类然后多个道具都继承自这个道具类类似工厂模式 public abstract class PowerUp : MonoBehaviour {public GameObject visualEffect; // 道具的视觉效果public AudioClip collectSound; // 收集音效public GameObject collectEffect; // 收集特效protected Car playerCar; // 玩家车辆引用protected GameManager gameManager; // 游戏管理器引用protected virtual void Start(){playerCar FindObjectOfTypeCar();}protected virtual void Update(){// 只检测玩家是否靠近靠近就触发if (playerCar ! null){float distance Vector3.Distance(transform.position, playerCar.transform.position);if (distance 2f){OnTriggerEnterManual(playerCar.gameObject);}}}protected virtual void OnTriggerEnterManual(GameObject other){if (other.CompareTag(Player)){ProcessPlayerCollision();}}protected virtual void OnTriggerEnter(Collider other){if (other.CompareTag(Player)){ProcessPlayerCollision();}}protected virtual void ProcessPlayerCollision(){// 播放收集音效if (collectSound ! null){AudioSource.PlayClipAtPoint(collectSound, transform.position);}// 生成收集特效if (collectEffect ! null){Instantiate(collectEffect, transform.position, Quaternion.identity);}// 激活道具效果ActivatePowerUp();// 立即销毁道具对象Destroy(gameObject);}// 道具效果激活protected abstract void ActivatePowerUp();// 道具效果结束不再需要 } 首先这是一个抽象类关键字abstarct且全部由虚函数构成。在Update中获取玩家操控的Car类的实例然后在Update中进行距离判断然后是两个Trigger相关的函数OnTriggerEnterManual和OnTriggerEnter用来判断发生Trigger的碰撞时另一个碰撞体的标签是否是Player是的话就执行道具效果函数ProcessPlayerCollision具体内容包括播放音效和视觉特效并执行ActivatePowerUp函数之后销毁道具——ActivatePowerUp函数则是一个纯虚函数要求所有继承PowerUp类的脚本必须实现。 香蕉皮没有美术素材就拿个黄色球意思一下。  protected override void ActivatePowerUp(){if (playerCar ! null){playerCar.ApplyBananaSlip(slipForce, rotationForce);}Debug.Log([Banana] 触发香蕉皮打滑效果);} 香蕉皮的ActivatePowerUp函数如图执行一个Car类中写好的香蕉皮打滑效果。 public void ApplyBananaSlip(float slipForce, float rotationForce){// 忽略参数值直接启动一个协程来处理香蕉皮效果StartCoroutine(DirectBananaEffect());} 执行相关协程   private IEnumerator DirectBananaEffect(){// 只记录原始旋转Quaternion originalRotation transform.rotation;// 晃动持续时间和强度float duration 2.0f;float intensity 30f; // 增加强度float elapsed 0f;Debug.Log([Car] 香蕉皮效果: 开始剧烈旋转!);// 晃动循环while (elapsed duration){// 计算当前强度 (波浪式变化不是线性衰减)float wave Mathf.Sin((elapsed / duration) * Mathf.PI * 6); // 创造波浪效果float currentIntensity intensity * Mathf.Abs(wave);// 随机旋转量主要在Y轴和Z轴float rotX Random.Range(-1f, 1f) * currentIntensity * 0.4f; // X轴轻微旋转float rotY Random.Range(-2f, 2f) * currentIntensity; // Y轴强烈旋转float rotZ Random.Range(-1f, 1f) * currentIntensity * 0.7f; // Z轴中等旋转// 应用旋转 - 直接修改旋转而不是累积transform.rotation originalRotation * Quaternion.Euler(rotX, rotY, rotZ);// 更新时间elapsed Time.deltaTime;yield return null;}// 恢复原始旋转transform.rotation originalRotation;Debug.Log([Car] 香蕉皮效果: 旋转结束!);} 可以看到我们首先记录原始的旋转用一个四元数来记录以避免万向锁和提高计算效率然后在计时器小于设定的道具效果时间时我们去根据数学函数生成随机的一个不同轴的波动强度给到小车的旋转以实现香蕉皮打滑的效果最后再还原到之前的旋转。 加速球用于加速。 using UnityEngine; using System.Collections;public class SpeedBoostPowerUp : PowerUp {public float speedMultiplier 2.0f; // 速度提升倍数private WorldGenerator worldGenerator; // 世界生成器引用protected override void Start(){base.Start();// 获取WorldGenerator引用worldGenerator FindObjectOfTypeWorldGenerator();if (worldGenerator null){Debug.LogError([SpeedBoost] 严重错误: 未找到WorldGenerator);}Debug.Log($[SpeedBoost] 初始化完成速度倍数: {speedMultiplier});}protected override void ActivatePowerUp(){if (playerCar ! null){playerCar.StartSpeedBoost(5f, speedMultiplier); // 5秒加速可根据需要调整}Debug.Log([SpeedBoost] 触发加速效果);} } 与香蕉皮类似我们也去调用car类里写好的加速函数然后需要注意的是我们实现这个效果比如要一个世界生成器类WorldGenerator类因为这个项目比较特殊我们的赛道是一个无限生成的赛道而这个赛道其实本质上是由两个不同的世界片段不断地交替更新实现的。在我们的加速过程中其实真正加速的不是赛车而是我们小车行驶的地面——想象一下跑步机就好了真正加速的是我们的跑步机而不是我们的人我们人的绝对位置其实是没有移动的。 private Coroutine speedBoostCoroutine; ...public void StartSpeedBoost(float duration, float speedMultiplier) {if (speedBoostCoroutine ! null){StopCoroutine(speedBoostCoroutine);EndSpeedBoost();}speedBoostCoroutine StartCoroutine(SpeedBoostRoutine(duration, speedMultiplier)); } 检测是否曾通过该变量启动过协程​如果启动过就强制终止该协程这样设计的意义是确保同一时间只有一个速度提升效果在运行新效果会强制中断并覆盖旧效果。 private IEnumerator SpeedBoostRoutine(float duration, float speedMultiplier){isSpeedBoosting true;WorldGenerator generator FindObjectOfTypeWorldGenerator();if (generator ! null){originalGlobalSpeed generator.globalSpeed;float newSpeed originalGlobalSpeed * speedMultiplier;generator.SetGlobalSpeed(newSpeed);Debug.Log($[Car] SpeedBoost: 世界速度提升到 {newSpeed});}yield return new WaitForSeconds(duration);EndSpeedBoost();}private void EndSpeedBoost(){isSpeedBoosting false;WorldGenerator generator FindObjectOfTypeWorldGenerator();if (generator ! null){generator.SetGlobalSpeed(originalGlobalSpeed);Debug.Log($[Car] SpeedBoost: 世界速度恢复到 {originalGlobalSpeed});}} 可以看到我们会去修改WorldGenerator的globalSpeed来实现加速的效果加速的协程通过yield return new WaitForSeconds(duration)来控制时长。 护盾道具给予小车短暂的无敌时间。 using UnityEngine; using System.Collections;public class ShieldPowerUp : PowerUp {private Car car;protected override void Start(){base.Start();car FindObjectOfTypeCar();}protected override void ActivatePowerUp(){if (car ! null){car.StartInvincible(5f); // 5秒无敌可根据需要调整Debug.Log([护盾] 无敌模式已开启);// 不再创建任何球体或视觉效果}} } 调用Car类中的无敌方法 public void StartInvincible(float duration){if (invincibleCoroutine ! null){StopCoroutine(invincibleCoroutine);EndInvincible();}invincibleCoroutine StartCoroutine(InvincibleRoutine(duration));// 开启护盾视觉特效if (shieldEffect ! null)shieldEffect.SetActive(true);if (statusIndicator ! null)statusIndicator.SetActive(true);} 类似于之前加速的协程的设计保证同一时刻不会有两个道具效果的叠加。 private IEnumerator InvincibleRoutine(float duration){isInvincible true;Debug.Log($[Car] Invincible: 无敌模式开启持续{duration}秒);yield return new WaitForSeconds(duration);EndInvincible();}public void EndInvincible(){isInvincible false;Debug.Log([Car] Invincible: 无敌模式关闭);// 关闭护盾视觉特效if (shieldEffect ! null)shieldEffect.SetActive(false);if (statusIndicator ! null)statusIndicator.SetActive(false);} 可以看到在协程中我们通过修改bool变量isInvincible来开启或关闭无敌效果这个无敌模式我们通过这样实现 void OnCollisionEnter(Collision collision){if (collision.gameObject.CompareTag(Obstacle)){Debug.Log($[车辆] 碰撞到障碍物: {collision.gameObject.name});if (invincibleMode || isInvincible){Debug.Log([车辆] 处于无敌模式销毁障碍物);Destroy(collision.gameObject); // 销毁障碍物return;}Debug.Log([车辆] 没有任何保护车辆将散架);FallApart();}} 当我们的车辆遇到障碍物时如果我们处于障碍物时直接去销毁路障否则我们就执行FallApart函数直接散架。 这样大体上我们的道具就实现了现在我们来看看世界生成器WorldGenerator是如何实现的 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering;public class WorldGenerator : MonoBehaviour {//一系列变量void Start(){// 创建数组用于存储每个世界部分的起始顶点用于正确过渡// 先生成两个世界部分}void LateUpdate(){// 如果第二个部分已经接近玩家移除第一个部分并更新世界// 更新场景中的所有物品如障碍物和大门}void UpdateAllItems(){// 同时查找所有带有 Item 和 Obstacle 标签的物品// 处理所有Item标签的物品// 处理所有Obstacle标签的物品}// 处理物体的MeshRenderer组件private void ProcessRenderers(GameObject[] objects){}//生成世界片段void GenerateWorldPiece(int i){}IEnumerator UpdateWorldPieces(){}//更新世界分块void UpdateSinglePiece(GameObject piece){}//创造圆柱形的赛道public GameObject CreateCylinder(){}// 生成并返回新世界部分的网格Mesh Generate(){}//生成地形void CreateShape(ref Vector3[] vertices, ref Vector2[] uvs, ref int[] triangles){}生成所有赛道内物体包括障碍物和道具void CreateItem(Vector3 vert, int x){}// 生成道具的方法private GameObject CreatePowerUp(Vector3 position, Transform parent){}// 道具清理方法void ClearPowerUpsOnPiece(GameObject piece){}// 设置全局速度并更新所有地形片段的速度public void SetGlobalSpeed(float newSpeed){}// 返回第一个世界部分的 Transformpublic Transform GetWorldPiece(){}// Destory函数void OnDestroy(){}}可以看到非常多的内容我们一点一点来介绍。 void Start(){// 创建数组用于存储每个世界部分的起始顶点用于正确过渡beginPoints new Vector3[(int)dimensions.x 1];// 先生成两个世界部分for (int i 0; i 2; i){GenerateWorldPiece(i);}} void LateUpdate(){// 如果第二个部分已经接近玩家移除第一个部分并更新世界if (pieces[1] pieces[1].transform.position.z 0)StartCoroutine(UpdateWorldPieces());// 更新场景中的所有物品如障碍物和大门UpdateAllItems();} 这里可以看到的一个细节是我们要在LateUpdate中执行更新世界片段和物体的操作而不是在Update中这是因为我们的其他脚本中可能涉及到位置的变换等等我们必须要等待其他相关脚本中的Update执行完之后再去计算距离来更新物品和世界片段。 void UpdateAllItems(){// 同时查找所有带有 Item 和 Obstacle 标签的物品GameObject[] items GameObject.FindGameObjectsWithTag(Item);GameObject[] obstacles GameObject.FindGameObjectsWithTag(Obstacle);// 处理所有Item标签的物品ProcessRenderers(items);// 处理所有Obstacle标签的物品ProcessRenderers(obstacles);}// 处理物体的MeshRenderer组件private void ProcessRenderers(GameObject[] objects){for (int i 0; i objects.Length; i){// 获取物品的所有 MeshRendererforeach (MeshRenderer renderer in objects[i].GetComponentsInChildrenMeshRenderer()){// 如果物品距离玩家足够近则显示该物品bool show objects[i].transform.position.z showItemDistance;// 如果需要显示物品更新其阴影投射模式// 由于世界是圆柱形的只有底半部分的物体需要阴影if (show)renderer.shadowCastingMode (objects[i].transform.position.y shadowHeight) ? ShadowCastingMode.On : ShadowCastingMode.Off;// 只有在需要显示物品时才启用其渲染器renderer.enabled show;}}} UpdateAllItems则是去游戏场景中找到所有带有Item和Obstacle标签的物体执行ProcessRenderers函数:这个函数的作用就是去获取物体的MeshRenderer并判断该GameObject类物体与设定的showItemDistance是否更小更小则渲染且再判断该物体是否位于圆柱底部位于底部则渲染影子。 关于MeshRenderer的shadowCastingMode void GenerateWorldPiece(int i){pieces[i] CreateCylinder();pieces[i].transform.Translate(Vector3.forward * (dimensions.y * scale * Mathf.PI) * i);UpdateSinglePiece(pieces[i]);Debug.Log($[WorldGen] 生成片段 {i}位置z{pieces[i].transform.position.z});}生成世界片段的函数可以看到输入参数是一个序号表示当前世界片段的序号先创造一个圆柱体然后把这个圆柱体的transform移动到当前y轴乘以scale和pai最后再乘以序号方向是正前方向不难看出这个是我们的周长公式而为什么要把新的圆柱生成在周长处呢dimension的y其实对应的就是我们Unity坐标系中的Z轴scale是不同顶点的间隔所以我们其实就是在拿z轴的顶点数再乘以顶点间隔之后再乘以pai至于这个pai只是我们在生成顶点时就乘以的系数并没有什么额外的几何意义。 IEnumerator UpdateWorldPieces(){Debug.Log($[WorldGen] 触发片段更新销毁片段0片段1位置z{pieces[1].transform.position.z}startObstacleChance{startObstacleChance});ClearPowerUpsOnPiece(pieces[0]);Destroy(pieces[0]);pieces[0] pieces[1];pieces[1] CreateCylinder();pieces[1].transform.position pieces[0].transform.position Vector3.forward * (dimensions.y * scale * Mathf.PI);pieces[1].transform.rotation pieces[0].transform.rotation;UpdateSinglePiece(pieces[1]);Debug.Log($[WorldGen] 新片段生成完成片段1位置z{pieces[1].transform.position.z}dimensions{dimensions}scale{scale});yield return 0;} 这是我们更新世界片段的协程可以看到我们其实只有两个世界片段我们销毁第一个世界片段并把第二个世界片段换到第一个世界片段并重新生成圆柱position和rotation进行更新。 void UpdateSinglePiece(GameObject piece){// 给新生成的部分添加基本运动脚本使其朝向玩家移动BasicMovement movement piece.AddComponentBasicMovement();// 设置其移动速度为 globalSpeed负数表示朝玩家方向移动movement.movespeed -globalSpeed;// 设置旋转速度为灯光方向光的旋转速度if (lampMovement ! null)movement.rotateSpeed lampMovement.rotateSpeed;// 为此部分创建一个终点GameObject endPoint new GameObject();endPoint.transform.position piece.transform.position Vector3.forward * (dimensions.y * scale * Mathf.PI);endPoint.transform.parent piece.transform;endPoint.name End Point;// 改变 Perlin 噪声的偏移量以确保每个世界部分与上一个不同offset randomness;} 我们给新的世界片段手动添加新的脚本组件Unity中的脚本就是组件BasicMovement让这个世界片段也能移动然后根据前向偏移量提前设置好终点最后修改柏林噪声相关偏移量以保证不同世界部分的柏林噪声不同。 public GameObject CreateCylinder(){// 创建世界部分的基础对象并命名GameObject newCylinder new GameObject();newCylinder.name World piece;// 设置当前圆柱体为新创建的对象currentCylinder newCylinder;// 给新部分添加 MeshFilter 和 MeshRenderer 组件MeshFilter meshFilter newCylinder.AddComponentMeshFilter();MeshRenderer meshRenderer newCylinder.AddComponentMeshRenderer();// 给新部分设置材质meshRenderer.material meshMaterial;// 生成网格并赋值给 MeshFiltermeshFilter.mesh Generate();// 添加与网格匹配的 MeshCollider 组件newCylinder.AddComponentMeshCollider();return newCylinder;} 这是创建新的圆柱形相关的代码我们生成一个新的GameObject并添加MeshFilter和MeshRenderer之后去更改部分组件内容之后返回。 在Unity中MeshFilter和MeshRenderer是渲染3D模型的核心组件二者协同工作以实现模型的几何形状定义与可视化渲染 比较简单地说MeshFilter通过自定义的方式定义网格属性之后把网格传给MeshRenderer——后者负责根据材质实现渲染。 可以看到给到MeshFilter的Mesh是由Generate函数生成的 Mesh Generate(){// 创建并命名新网格Mesh mesh new Mesh();mesh.name MESH;// 创建数组来存储顶点、UV 坐标和三角形Vector3[] vertices null;Vector2[] uvs null;int[] triangles null;// 创建网格形状并填充数组CreateShape(ref vertices, ref uvs, ref triangles);// 给网格赋值mesh.vertices vertices;mesh.uv uvs;mesh.triangles triangles;// 重新计算法线mesh.RecalculateNormals();return mesh;} 这其中的核心部分显然是这个CreateShape void CreateShape(ref Vector3[] vertices, ref Vector2[] uvs, ref int[] triangles){// 获取该部分在 x 和 z 轴的大小int xCount (int)dimensions.x; // 在 x 轴上分割的顶点数量int zCount (int)dimensions.y; // 在 z 轴上分割的顶点数量// 初始化顶点和 UV 数组vertices new Vector3[(xCount 1) * (zCount 1)];uvs new Vector2[(xCount 1) * (zCount 1)];int index 0;// 获取圆柱体的半径float radius xCount * scale * 0.5f; // 圆柱的半径// 双重循环遍历 x 和 z 轴的所有顶点for (int x 0; x xCount; x){for (int z 0; z zCount; z){// 获取圆柱体的角度以正确设置顶点位置float angle x * Mathf.PI * 2f / xCount;// 使用角度的余弦和正弦值来设置顶点vertices[index] new Vector3(Mathf.Cos(angle) * radius, Mathf.Sin(angle) * radius, z * scale * Mathf.PI);// 更新 UV 坐标uvs[index] new Vector2(x * scale, z * scale);// 使用 Perlin 噪声生成 X 和 Z 值float pX (vertices[index].x * perlinScale) offset;float pZ (vertices[index].z * perlinScale) offset;// 将顶点移动到中心位置保持 z 坐标并使用 Perlin 噪声调整位置Vector3 center new Vector3(0, 0, vertices[index].z);vertices[index] (center - vertices[index]).normalized * Mathf.PerlinNoise(pX, pZ) * waveHeight;// 处理世界部分之间的平滑过渡if (z startTransitionLength beginPoints[0] ! Vector3.zero){// 如果是过渡部分结合 Perlin 噪声和上一个部分的起始点float perlinPercentage z * (1f / startTransitionLength);Vector3 beginPoint new Vector3(beginPoints[x].x, beginPoints[x].y, vertices[index].z);vertices[index] (perlinPercentage * vertices[index]) ((1f - perlinPercentage) * beginPoint);}else if (z zCount){// 更新起始点以确保下一部分的平滑过渡beginPoints[x] vertices[index];}if (z % 10 0) // 每10个单位记录一次避免日志过多{// Debug.Log($[WorldGen] 当前位置 x{x}, z{z}, 实际z{vertices[index].z}, startObstacleChance{startObstacleChance});}if (Random.Range(0, startObstacleChance) 0 !(gate null obstacles.Length 0)){//Debug.Log($[WorldGen] CreateShape 生成障碍物判定通过z{vertices[index].z}, startObstacleChance{startObstacleChance});CreateItem(vertices[index], x);}// 增加顶点索引index;}}// 初始化三角形数组triangles new int[xCount * zCount * 6]; // 每个方格有 2 个三角形每个三角形由 3 个顶点组成共 6 个顶点// 创建每个方块的基础三角形的组成更简单int[] boxBase new int[6]; // 每个正方形面由 6 个顶点组成两个三角形int current 0;// 遍历 x 轴上的所有位置for (int x 0; x xCount; x){boxBase new int[]{x * (zCount 1),x * (zCount 1) 1,(x 1) * (zCount 1),x * (zCount 1) 1,(x 1) * (zCount 1) 1,(x 1) * (zCount 1),};// 遍历 z 轴上的所有位置for (int z 0; z zCount; z){// 增加顶点索引并创建三角形for (int i 0; i 6; i){boxBase[i] boxBase[i] 1;}// 使用六个顶点填充三角形for (int j 0; j 6; j){triangles[current j] boxBase[j] - 1;}// 增加当前索引current 6;}}} 这个就是我们基于柏林噪声生成网格的核心代码了首先根据输入的网格密度参数xCount为圆周方向分段数zCount为隧道长度分段数计算顶点坐标——利用三角函数Mathf.Cos(angle) * radius和Mathf.Sin(angle) * radius将顶点沿圆周分布并通过z * scale * Mathf.PI沿Z轴延伸形成螺旋结构同时为每个顶点生成UV坐标以支持纹理映射随后通过Perlin噪声​Mathf.PerlinNoise(pX, pZ) * waveHeight对顶点位置施加随机扰动模拟隧道表面的自然凹凸并采用平滑过渡机制——当顶点位于起始过渡区z startTransitionLength时与上一片段记录的端点beginPoints[x]进行插值混合以消除接缝并在片段末端z zCount更新beginPoints供后续片段衔接使用此外在特定位置如z % 10 0按概率startObstacleChance生成障碍物调用CreateItem增加场景交互性最后通过双重循环构建三角形索引数组将每4个顶点拆分为2个三角形6个索引以boxBase模板填充索引数据形成连续的三维曲面。 可能看起来有些复杂我们可以先想象我们有一个平摊的网格网格由一堆顶点组成——顶点的个数由相关的网格密度参数xCount和zCount决定x轴对应圆柱体周长而z轴对应圆柱体长度。我们现在像卷大饼一样把这个平摊的网格乘以一个三角函数来将这个网格平面变成一个圆柱体然后我们将这个卷起来的卷饼内部的不同顶点根据柏林噪声改变顶点位置最后加上我们的MeshColllider即可。 void CreateItem(Vector3 vert, int x){Debug.Log($[WorldGen] CreateItem 被调用z{vert.z});// 获取圆柱体的中心位置但使用顶点的 z 坐标Vector3 zCenter new Vector3(0, 0, vert.z);// 检查生成物品的正确位置优化判断条件if (zCenter - vert Vector3.zero ||(x (int)dimensions.x / 4 Mathf.Abs(vert.y) 0.1f) ||(x (int)dimensions.x / 4 * 3 Mathf.Abs(vert.y) 0.1f)){// Debug.Log($[WorldGen] 跳过物品生成 - zCenter-vert: {zCenter-vert}, x: {x}, dimensions.x/4: {(int)dimensions.x/4});return;}GameObject newItem null;// 调整道具生成逻辑确保更均匀的分布bool shouldCreatePowerUp Random.Range(0, 100) powerUpChance powerUps ! null powerUps.Length 0;if (shouldCreatePowerUp){newItem CreatePowerUp(vert, currentCylinder.transform);if (newItem null){shouldCreatePowerUp false;}}if (!shouldCreatePowerUp){bool isGate (Random.Range(0, gateChance) 0);// if (isGate)// Debug.Log([WorldGen] 生成大门);// else// Debug.Log([WorldGen] 生成障碍物);newItem Instantiate(isGate ? gate : obstacles[Random.Range(0, obstacles.Length)]);newItem.transform.rotation Quaternion.LookRotation(zCenter - vert, Vector3.up);newItem.transform.position vert;newItem.transform.SetParent(currentCylinder.transform, false);}}// 生成道具的方法private GameObject CreatePowerUp(Vector3 position, Transform parent){if (powerUps null || powerUps.Length 0) return null;// 检查是否与上一个道具距离太近if (Mathf.Abs(position.z - lastPowerUpZ) minPowerUpSpacing){return null;}// 随机选择一个道具预制体GameObject powerUpPrefab powerUps[Random.Range(0, powerUps.Length)];// 计算道具应该放置的正确高度// 使用顶点的位置并添加一个固定高度这样道具就不会嵌入地形或悬在空中Vector3 spawnPosition position;// 稍微提高道具的位置使其位于地形上方spawnPosition.y powerUpHeight;// 实例化道具GameObject powerUp Instantiate(powerUpPrefab, spawnPosition, Quaternion.identity);// 设置父物体powerUp.transform.SetParent(parent);// 添加到活动道具列表activePowerUps.Add(powerUp);// 更新最后一个道具的Z坐标lastPowerUpZ (int)position.z;return powerUp;}// 修改清理方法确保道具被正确销毁前不触发效果void ClearPowerUpsOnPiece(GameObject piece){ListGameObject powerUpsToRemove new ListGameObject();foreach (GameObject powerUp in activePowerUps){if (powerUp null || (powerUp.transform.parent ! null powerUp.transform.parent.gameObject piece)){powerUpsToRemove.Add(powerUp);if (powerUp ! null){// 直接销毁道具对象不再调用MarkAsClearedDestroy(powerUp);}}}foreach (GameObject powerUp in powerUpsToRemove){activePowerUps.Remove(powerUp);}} 最后是道具生成的方法注释写得比较详细这几段代码主要负责在圆柱形赛道上生成和管理游戏物品包括障碍物、大门和道具。CreateItem方法负责在合适的位置生成物品它会检查生成位置是否合适并根据概率决定生成道具还是障碍物同时确保物品朝向正确CreatePowerUp方法专门处理道具的生成它会检查道具之间的间距设置合适的高度并管理道具的生命周期而ClearPowerUpsOnPiece方法则负责在不需要时安全地清理特定世界片段上的所有道具。这些代码共同确保了赛道上物品的合理分布、正确朝向和及时清理为玩家提供流畅的游戏体验。 至此我们这个项目的大多数内容就讲解完了让我们看看最开始的几个问题如何解答 如何实现无限的赛道 答我们其实是基于类似于内存池的方式来做的真正的世界片段只有两个但是我们通过每生成一个新世界片段时就通过可以计算的前向偏移得到终点后在LateUpdate中检测距离当距离小于一定值时就生成新的圆柱形世界片段并更新世界片段即可。 如何基于柏林噪声实现地形 答柏林噪声本身是Unity自带的内容我们的赛道本质上是一个更改了顶点位置的网格我们通过柏林噪声去修改顶点的具体位置即可实现波浪状的地形。 如何创造圆柱形的赛道 答这个和我们的程序生成地形有关我们程序化地形的过程是先生成一定顶点数的网格然后通过三角函数沿着X轴修改顶点位置以实现圆柱形网格。 如何实现道具效果 答因为本质上我们的赛车游戏中是世界片段在移动而不是小车在移动所以主要是加速部分需要去更改世界片段的速度其他的道具效果都是直接基于小车的属性进行更改比较简单。 但这里又引出一个问题就是为何我们让世界片段移动而不是让小车移动 这个项目选择让世界片段移动而不是让小车移动主要是出于以下几个考虑首先在无限赛道的设计中如果让小车移动需要不断生成新的赛道片段并销毁旧的片段这样会导致频繁的内存分配和释放影响性能其次让世界片段移动可以保持小车始终在视野中心这样更容易控制视角和实现特效如滑痕、草地特效等第三这种设计使得赛道的生成和销毁更加可控可以精确地控制新片段的生成时机和位置确保赛道的连续性和平滑性最后这种设计也简化了物理模拟因为只需要处理相对运动而不需要考虑小车高速移动时可能带来的物理计算问题。 这个回答是AI生成的孰对孰错大家自行判断吧。
http://www.hkea.cn/news/14439486/

相关文章:

  • 做网站需要好多钱上海网站建设聚众网络
  • 建立网站外链常用的渠道有哪些建设网站需要的人员及资金
  • 暂时没有域名怎么做网站网站建设与维护教学课件
  • 学校网站定位网站建设技术难题
  • 免费设计模板网站个人简历怎么做
  • 网站 注册模块怎么做烟台logo设计公司
  • 南昌定制网站开发费用店面设计的重要性
  • 优秀个人网站欣赏邢台建设网站
  • 重庆网站建设 微客巴巴大公司做网站的优势
  • 杭州网站制作方法网站打不开原因检测
  • 湘潭网站建设企业广东东莞免费网站制作公司
  • 网站怎么静态化东营外贸型网站设计
  • 舆情网站网址建设部网站221号文件
  • 我想做个网站老山做网站的公司
  • 厦门市同安区建设局公开网站网站直播的功能怎样做
  • 中济建设官方网站万网网站建设的子分类能显示多少个
  • 网站运营繁忙手机网站的建设价格
  • 北京网站seo排名跨境电商自己做网站卖衣服
  • 做品牌网站哪个好点网站页脚内容
  • 如何做幸运28网站代理福建 网站建设
  • 微商网站推广怎么做怎么知道公司网站是哪个公司做的
  • 怎样建个网站wordpress 批量注册
  • 丰县住房与城乡建设部网站网站备案的幕布是什么来的
  • 手机购物网站开发延吉市住房城乡建设局网站
  • 百度手机网站优化指南亳州网站建设推广
  • 许昌市做网站做模具五金都是用的那个网站
  • 昆山网站建设推广郑州做网站哪家最好
  • 如何判断网站seo做的好坏模板创作师
  • 蓝色大气企业网站phpcms模板免费的个人简历电子版
  • 电商培训网站辽宁男科医院排名最好的医院