HTML5 game Development Summary

本文分享了一款HTML5游戏的开发经验,详细介绍了游戏对象模块的建立与实例化,包括蘑菇、熊和奖品等角色的特性设定。游戏架构解析及碰撞检测机制,如小熊与边界、蘑菇和奖品的碰撞逻辑,为初学者提供了实用的代码示例。

最近跟着html5中文网的教程写了个HTML5的游戏,做个小结啦!先给个游戏截图

一、建立的对象模块

该部分包含游戏中各个对象的建立和实例化

1、公用的游戏对象(public Object)部分——其他对象均继承Inherit该对象

function GameObject()       
{       
    this.x = 0;//x 坐标      
    this.y = 0;//y 坐标      
    this.image = null; //图片      
}  

2、蘑菇对象

function Mushroom() {};       
Mushroom.prototype = new GameObject();
var mushroom = new Mushroom(); //蘑菇实例化

3、熊对象——加一个angle属性,表示熊的选择角度

function Animal() {};     
Animal.prototype = new GameObject();
Animal.prototype.angle = 0;//旋转的角度,(用来改变熊的旋转速度) 
//定义熊实例      
var animal = new Animal(); 

4、奖品对象——加了四个属性

function Prize() {};   
Prize.prototype = new GameObject();
Prize.prototype.row = 0;//奖品行位置   
Prize.prototype.col = 0;//奖品列位置   
Prize.prototype.hit = false;//是否被撞过   
Prize.prototype.point = 0;//分数 

同时定义了一个奖品数组(因为奖品比较多)

var prizes = new Array();

此时奖品的初始化也单独写了一个function实现(共三行、每行23个奖品,也就是实例化了69个奖品对象,(⊙o⊙))

    function InitPrizes()
    {
        var count=0;
        for(var x=0; x<3; x++)
        {
            for(var y=0; y<23; y++)
            {
                prize = new Prize();
                if(x==0)
                    prize.image = flowerImg;//flower in 1 row
                     prize.point = 3;
                if(x==1)
                    prize.image = acornImg;//acorn in 2 row
                    prize.point = 2;
                if(x==2)
                    prize.image = leafImg;//leaf in 3 row
                    prize.point = 2;
                prize.row = x;
                prize.col = y;
                prize.x = 20 * prize.col + 10;
                prize.y = 30 * prize.row + 40;
                //撞到奖品数组,用来描绘
                prizes[count] = prize;
                count++;
            }
        }
    }

5、其他

var lives=3;  //生命个数
var livesImages = new Array();//生命图片数组

var score = 0;  //得分

二、整个游戏架构

该游戏并不复杂,让我们从游戏开始执行分析,既从jQuery的$(window).ready(function(){});开头。

三、碰撞检测部分

游戏中检测了三个碰撞: 小熊和边界HasAnimalHitEdge()、小熊和蘑菇HasAnimalHitMushroom()、小熊和奖品HasAnimalHitPrize()

其中小熊和边界HasAnimalHitEdge()单独检测,其他两个通过调用一个通用的检测函数CheckIntersect()来判断是否碰撞

1、小熊和边界HasAnimalHitEdge()

function HasAnimalHitEdge()   
{   
    //熊碰到右边边界   
    if(animal.x>screenWidth - animal.image.width)   
    {   
        if(horizontalSpeed > 0)//假如向右移动   
            horizontalSpeed =-horizontalSpeed;//改变水平速度方向   
    }   
    //熊碰到左边边界   
    if(animal.x<-10)   
    {   
        if(horizontalSpeed < 0)//假如向左移动   
            horizontalSpeed = -horizontalSpeed;//改变水平速度方向   
    }   
    //熊碰到下面边界   
    if(animal.y>screenHeight - animal.image.height)   
    {   
        //2秒钟后从新开始   
        setTimeout(function(){   
            horizontalSpeed = speed;   
            verticalSpeed = -speed;   
            animal.x = parseInt(screenWidth/2);   
            animal.y = parseInt(screenHeight/2);   
            gameLoop();   
        }, 2000);   
    }   
    //熊碰到上边边界   
    if(animal.y<0)   
    {   
        verticalSpeed = -verticalSpeed;   
    }   
}

小熊碰到下边界的时候比较复杂一点,必须改变其垂直方向为相反方向,还必须判断如果没有碰撞既游戏小熊生命减少一次,并重新开始游戏

2、通用的检测函数CheckIntersect()

其基本原理如图所示,如果发生碰撞返回true,否则返回false

unction CheckIntersect(object1, object2, overlap)   
{   
    A1 = object1.x + overlap;   
    B1 = object1.x + object1.image.width - overlap;   
    C1 = object1.y + overlap;   
    D1 = object1.y + object1.image.height - overlap;   
    
    A2 = object2.x + overlap;   
    B2 = object2.x + object2.image.width - overlap;   
    C2 = object2.y + overlap;   
    D2 = object2.y + object2.image.width - overlap;   
    
    //假如在x-轴重叠   
    if(A1 > A2 && A1 < B2   
       || B1 > A2 && B1 < B2)   
    {   
        //判断y-轴重叠   
        if(C1 > C2 && C1 < D1   
       || D1 > C2 && D1 < D2)   
        {   
            //碰撞   
            return true;   
        }   
    }   
    return false;   
}  

3.小熊和蘑菇碰撞

function HasAnimalHitMushroom()   
{   
    //假如碰撞   
    if(CheckIntersect(animal, mushroom, 5))   
    { 
        if((animal.x + animal.image.width/2) < (mushroom.x + mushroom.image.width*0.25))   
        {   horizontalSpeed = -speed; } 
        else if((animal.x + animal.image.width/2) < (mushroom.x + mushroom.image.width*0.5))   
        {  horizontalSpeed = -speed/2; }   
       else if((animal.x + animal.image.width/2) < (mushroom.x + mushroom.image.width*0.75))   
        { horizontalSpeed = speed/2;  }   
        else  
        {   horizontalSpeed = speed; }   
        verticalSpeed = -speed;  //改变垂直速度。也即动物向上移动     
    }   
}  

原理如图所示

注解:我有点不是很明白这个判断条件,为什么这样判断呢??????

4. 小熊和奖品碰撞

function HasAnimalHitPrize()   
{   
    //取出所有奖品   
    for(var x=0; x<prizes.length; x++)   
    {   
        var prize = prizes[x];   
        //假如没有碰撞过,则描绘在画布上   
        if(!prize.hit)   
        {   
            if(CheckIntersect(prize, animal, 0))   
            {   
                prize.hit = true;                     
                verticalSpeed = speed;   //熊反弹下沉 
            }   
        }   
    }   
} 
 
 


转载于:https://www.cnblogs.com/JoannaQ/archive/2012/09/12/2681309.html

using System; using System.Collections; using UnityEngine; using UnityEngine.UI; using TMPro; public class StepStartController : MonoBehaviour { // 事件:UI收起动画完成时触发 public event Action OnAnimationComplete; // 事件:UI展开动画完成时触发 public event Action OnShowAnimationComplete; [Header("UI元素引用")] public GameObject mainTip; // 主提示面板 public GameObject backGround; // 背景 public TMP_Text tipText; // 提示文本 public TMP_Text stepTipsText; // 步骤文本 [Header("位置设置")] public Vector3 show; // 显示位置 public Vector3 after; // 收起后位置 public float fontsizeHigh; public float fontsizeLow; private CanvasGroup backgroundCanvas; // 背景画布组 private CanvasGroup tipTextCanvas; // 标题文本画布组 private RectTransform mainTipRect; // 主提示面板的RectTransform private Coroutine activeAnimation; // 当前活动的动画协程 private string originalTipText; // 原始提示文本 private void Awake() { // 获取必要的组件引用 backgroundCanvas = backGround.GetComponent<CanvasGroup>(); tipTextCanvas = tipText.GetComponent<CanvasGroup>(); mainTipRect = mainTip.GetComponent<RectTransform>(); // 保存原始提示文本 originalTipText = tipText.text; } void Start() { // 初始化UI状态 mainTipRect.sizeDelta = new Vector2(600, mainTipRect.sizeDelta.y); mainTipRect.anchoredPosition3D = show; backgroundCanvas.alpha = 1; tipTextCanvas.alpha = 1; stepTipsText.fontSize = fontsizeHigh; backGround.SetActive(true); tipText.gameObject.SetActive(true); } /// <summary> /// 更新倒计时文本 /// </summary> /// <param name="seconds">剩余秒数</param> public void UpdateCountdownText(int seconds) { tipText.text = $"{seconds}秒后开始该步骤"; } /// <summary> /// (外部调用)点击缩小后进行步骤 /// </summary> public void AfterClick() { // 停止正在进行的动画 if (activeAnimation != null) { StopCoroutine(activeAnimation); } activeAnimation = StartCoroutine(AfterClickAnimation()); } /// <summary> /// (外部调用)步骤结束后 /// </summary> public void ShowUIStart() { // 恢复原始提示文本 tipText.text = originalTipText; // 停止正在进行的动画 if (activeAnimation != null) { StopCoroutine(activeAnimation); } activeAnimation = StartCoroutine(ShowUIAnimation()); } /// <summary> /// 点击缩小后窗口运作协程 /// </summary> private IEnumerator AfterClickAnimation() { float duration = 1f; // 动画持续时间 float elapsed = 0f; // 已用时间 // 记录初始状态 Vector3 startPos = mainTipRect.anchoredPosition3D; Vector2 startSize = mainTipRect.sizeDelta; float bgStartAlpha = backgroundCanvas.alpha; float textStartAlpha = tipTextCanvas.alpha; // 目标状态 Vector3 targetPos = after; Vector2 targetSize = new Vector2(351, startSize.y); float bgTargetAlpha = 0f; float textTargetAlpha = 0f; // 动画插值 while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); // 同时进行所有动画 mainTipRect.anchoredPosition3D = Vector3.Lerp(startPos, targetPos, t); mainTipRect.sizeDelta = Vector2.Lerp(startSize, targetSize, t); backgroundCanvas.alpha = Mathf.Lerp(bgStartAlpha, bgTargetAlpha, t); tipTextCanvas.alpha = Mathf.Lerp(textStartAlpha, textTargetAlpha, t); yield return null; } // 确保到达最终状态 mainTipRect.anchoredPosition3D = targetPos; mainTipRect.sizeDelta = targetSize; backgroundCanvas.alpha = bgTargetAlpha; tipTextCanvas.alpha = textTargetAlpha; // 禁用对象 backGround.SetActive(false); tipText.gameObject.SetActive(false); stepTipsText.fontSize = fontsizeLow; // 触发收起动画完成事件 OnAnimationComplete?.Invoke(); } /// <summary> /// 新步骤开始时窗口运作协程 /// </summary> private IEnumerator ShowUIAnimation() { // 激活UI元素 backGround.SetActive(true); tipText.gameObject.SetActive(true); float duration = 1f; // 动画持续时间 float elapsed = 0f; // 已用时间 // 记录初始状态 Vector3 startPos = mainTipRect.anchoredPosition3D; Vector2 startSize = mainTipRect.sizeDelta; float bgStartAlpha = backgroundCanvas.alpha; float textStartAlpha = tipTextCanvas.alpha; // 目标状态(展开状态) Vector3 targetPos = show; Vector2 targetSize = new Vector2(600, startSize.y); float bgTargetAlpha = 1f; float textTargetAlpha = 1f; // 动画插值 while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); // 同时进行所有动画 mainTipRect.anchoredPosition3D = Vector3.Lerp(startPos, targetPos, t); mainTipRect.sizeDelta = Vector2.Lerp(startSize, targetSize, t); backgroundCanvas.alpha = Mathf.Lerp(bgStartAlpha, bgTargetAlpha, t); tipTextCanvas.alpha = Mathf.Lerp(textStartAlpha, textTargetAlpha, t); yield return null; } // 确保到达最终状态 mainTipRect.anchoredPosition3D = targetPos; mainTipRect.sizeDelta = targetSize; backgroundCanvas.alpha = bgTargetAlpha; tipTextCanvas.alpha = textTargetAlpha; stepTipsText.fontSize = fontsizeHigh; // 触发展开动画完成事件 OnShowAnimationComplete?.Invoke(); } /// <summary> /// 重置UI到初始状态 /// </summary> public void ResetToInitialState() { // 停止所有正在进行的动画 if (activeAnimation != null) { StopCoroutine(activeAnimation); activeAnimation = null; } // 重置UI元素状态 mainTipRect.sizeDelta = new Vector2(600, mainTipRect.sizeDelta.y); mainTipRect.anchoredPosition3D = show; backgroundCanvas.alpha = 1; tipTextCanvas.alpha = 1; stepTipsText.fontSize = fontsizeHigh; backGround.SetActive(true); tipText.gameObject.SetActive(true); // 恢复原始提示文本 tipText.text = originalTipText; } } using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; using System; using System.Collections; using System.Collections.Generic; using UnityEngine.XR.Interaction.Toolkit; public class TaskController : MonoBehaviour { [System.Serializable] // 抓取传送配置类:单个“抓取对象-传送目标-延迟时间”组合(已移除configTag) public class PickUpTele { [Header("触发抓取后传送")] public XRGrabInteractable grabInteractable; // 可抓取交互对象(触发条件) [Header("传送位置")] public GameObject teleportTarget; // 对应传送目标位置 [Header("传送延迟设置")] [Min(0)] // 限制延迟非负 public float teleportDelay = 0f; // 抓握后触发传送的等待时间(秒) } [System.Serializable] public class TaskStep { [Header("步骤设置")] public string name; // 步骤名称(UI显示) [TextArea] public string tip; // 步骤提示文本 [TextArea] public string stepEndText; // 步骤结束提示 public AudioClip stepStartAudio; // 步骤开始语音 public AudioClip stepEndAudio; // 步骤结束语音 public bool isTeleport = true; // 步骤默认传送开关(与多抓取传送独立) [Header("多抓取传送配置(可空/可多组)")] public PickUpTele[] pickUpTeleArray; // 改为数组:支持多组抓取传送配置 [Header("事件")] public UnityEvent onStepStart; // 步骤开始时触发的事件 public UnityEvent onStepEnd; // 步骤结束时触发的事件 } [Header("引用")] public StepManager stepManager; // 步骤UI管理器 public TeleportTo teleportTo; public Transform teleportTransform; // 步骤默认传送位置 public StepStartController stepStartController; // 步骤UI动画控制器 public GameObject popupPrefab; // 提示弹窗预制体 public AudioSource audioSource; // 语音播放器 public GameObject mainCanvas; // 主要UI public GameObject FinalOver; // 最后结束任务弹窗 public int stepStartTime = 5; // 步骤开始倒计时时间 public int stepOverTime = 5; // 步骤结束弹窗倒计时时间 [Header("步骤列表")] public TaskStep[] steps; // 任务步骤数组 // 多抓取传送状态管理(替换原单个配置变量) private List<PickUpTele> currentPickUpTeleList = new List<PickUpTele>(); // 当前步骤的有效配置列表 private List<Coroutine> pickUpCheckCoroutines = new List<Coroutine>(); // 多抓取检测协程列表 private List<Coroutine> delayedTeleportCoroutines = new List<Coroutine>(); // 多延迟传送协程列表 // 原有状态变量 private int currentStepIndex = -1; // 当前步骤索引 private GameObject currentPopup; // 当前显示的弹窗 private bool isWaitingForAnimation; // 等待UI动画完成的标志 private Coroutine countdownCoroutine; // 步骤开始倒计时协程 // 外部访问属性 public int CurrentStepIndex => currentStepIndex; public int TotalSteps => steps.Length; public string CurrentTip => (currentStepIndex >= 0 && currentStepIndex < steps.Length) ? steps[currentStepIndex].tip : ""; void OnDestroy() { CleanupAllPickUpTele(); // 清理所有抓取传送配置 // 清理UI事件监听 if (stepStartController != null) { stepStartController.OnAnimationComplete -= HandleAnimationComplete; stepStartController.OnShowAnimationComplete -= HandleShowAnimationComplete; } StopAllCoroutines(); } void OnDisable() { CleanupAllPickUpTele(); // 清理所有抓取传送配置 StopAllCoroutines(); // 清理UI事件监听 if (stepStartController != null) { stepStartController.OnAnimationComplete -= HandleAnimationComplete; stepStartController.OnShowAnimationComplete -= HandleShowAnimationComplete; } } /// <summary> /// 处理UI动画完成事件 /// </summary> private void HandleAnimationComplete() { if (isWaitingForAnimation && currentStepIndex >= 0 && currentStepIndex < steps.Length) { isWaitingForAnimation = false; steps[currentStepIndex].onStepStart?.Invoke(); Debug.Log($"[步骤{currentStepIndex + 1}] 步骤开始事件触发"); } } /// <summary> /// 处理UI展开动画完成事件 /// </summary> private void HandleShowAnimationComplete() { if (currentStepIndex >= 0 && currentStepIndex < steps.Length) { if (countdownCoroutine != null) StopCoroutine(countdownCoroutine); countdownCoroutine = StartCoroutine(StartCountdown()); } } /// <summary> /// 步骤开始前倒计时协程 /// </summary> private IEnumerator StartCountdown() { int countdownTime = stepStartTime; for (int i = countdownTime; i > 0; i--) { stepStartController.UpdateCountdownText(i); yield return new WaitForSeconds(1f); } stepStartController.AfterClick(); } /// <summary> /// 开始下一个步骤 /// </summary> public void StartNextStep() { // 1. 清理上一步的所有抓取传送配置 CleanupAllPickUpTele(); // 2. 结束当前步骤 if (currentStepIndex >= 0 && currentStepIndex < steps.Length) { steps[currentStepIndex].onStepEnd?.Invoke(); Debug.Log($"[步骤{currentStepIndex + 1}] 步骤结束事件触发"); } // 3. 切换到下一步 currentStepIndex++; if (currentStepIndex >= steps.Length) { Debug.Log("所有任务步骤已完成!"); return; } // 4. 执行新步骤(包含多抓取传送初始化) ExecuteCurrentStep(); } /// <summary> /// 执行当前步骤 /// </summary> private void ExecuteCurrentStep() { isWaitingForAnimation = true; TaskStep currentStep = steps[currentStepIndex]; // 1. 初始化当前步骤的多抓取传送配置 SetupMultiPickUpTele(currentStep); // 2. 更新步骤UI与语音 if (stepManager != null) { stepManager.UpdateStepVisuals(); stepStartController.ShowUIStart(); if (currentStep.stepStartAudio != null && audioSource != null) { audioSource.Stop(); audioSource.PlayOneShot(currentStep.stepStartAudio); } } // 3. 打印当前步骤抓取配置信息(调试用,已移除configTag关联) if (currentStep.pickUpTeleArray != null && currentStep.pickUpTeleArray.Length > 0) { Debug.Log($"[步骤{currentStepIndex + 1}] 加载 {currentStep.pickUpTeleArray.Length} 组抓取传送配置"); } else { Debug.Log($"[步骤{currentStepIndex + 1}] 无抓取传送配置"); } } /// <summary> /// 初始化当前步骤的多组抓取传送配置 /// </summary> private void SetupMultiPickUpTele(TaskStep currentStep) { // 清空原有列表(防止残留) currentPickUpTeleList.Clear(); pickUpCheckCoroutines.Clear(); delayedTeleportCoroutines.Clear(); // 若配置数组为空,直接返回(不启用抓取传送) if (currentStep.pickUpTeleArray == null || currentStep.pickUpTeleArray.Length == 0) return; // 遍历所有配置,初始化有效项 for (int i = 0; i < currentStep.pickUpTeleArray.Length; i++) { PickUpTele config = currentStep.pickUpTeleArray[i]; int configIndex = i; // 闭包捕获索引(避免循环变量问题) // 跳过无效配置(抓取对象或目标位置为空) if (config == null || config.grabInteractable == null || config.teleportTarget == null) { Debug.LogWarning($"[步骤{currentStepIndex + 1}] 第{configIndex + 1}组抓取传送配置无效(缺少抓取对象/目标位置)"); continue; } // 添加到有效配置列表 currentPickUpTeleList.Add(config); // 方式1:协程检测(推荐,每帧监控抓取状态,支持一一对应传送) Coroutine checkCoroutine = StartCoroutine(CheckSinglePickUpStatus(config, configIndex)); pickUpCheckCoroutines.Add(checkCoroutine); // 方式2:事件监听(备选,抓取时触发事件,需手动匹配配置) // config.grabInteractable.selectEntered.AddListener((args) => OnSingleGrabSelected(args, config, configIndex)); } } /// <summary> /// 清理当前步骤的所有抓取传送配置(防止内存泄漏/跨步骤触发) /// </summary> private void CleanupAllPickUpTele() { // 1. 停止所有抓取检测协程 foreach (Coroutine coro in pickUpCheckCoroutines) { if (coro != null) StopCoroutine(coro); } pickUpCheckCoroutines.Clear(); // 2. 停止所有延迟传送协程 foreach (Coroutine coro in delayedTeleportCoroutines) { if (coro != null) StopCoroutine(coro); } delayedTeleportCoroutines.Clear(); // 3. 移除所有事件监听(若使用事件方式) foreach (PickUpTele config in currentPickUpTeleList) { if (config != null && config.grabInteractable != null) { config.grabInteractable.selectEntered.RemoveAllListeners(); } } // 4. 清空配置列表 currentPickUpTeleList.Clear(); } /// <summary> /// 单组抓取配置的检测协程(一一对应:只检测当前配置的抓取对象,触发对应传送) /// </summary> private IEnumerator CheckSinglePickUpStatus(PickUpTele targetConfig, int configIndex) { // 持续检测直到配置失效或步骤切换 while (currentPickUpTeleList.Contains(targetConfig) && targetConfig.grabInteractable != null && targetConfig.teleportTarget != null) { // 检测到当前配置的物体被抓握 if (targetConfig.grabInteractable.isSelected) { // 处理传送延迟(已移除configTag关联,用配置索引区分) float actualDelay = Mathf.Max(0f, targetConfig.teleportDelay); Debug.Log($"[步骤{currentStepIndex + 1}-第{configIndex + 1}组] 抓握物体 {targetConfig.grabInteractable.name},{actualDelay}秒后传送至 {targetConfig.teleportTarget.name}"); if (actualDelay > 0f) { yield return new WaitForSeconds(actualDelay); } // 执行一一对应的传送 if (teleportTo != null) { teleportTo.Teleport(targetConfig.teleportTarget); Debug.Log($"[步骤{currentStepIndex + 1}-第{configIndex + 1}组] 已传送至 {targetConfig.teleportTarget.name}"); } // 可选:单次抓取只触发一次传送(若需重复抓取传送,删除下方break) break; } yield return null; // 每帧检测一次 } } /// <summary> /// 单组抓取配置的事件监听(备选方式:抓取时触发,一一对应传送) /// </summary> private void OnSingleGrabSelected(SelectEnterEventArgs args, PickUpTele targetConfig, int configIndex) { // 验证配置有效性 if (!currentPickUpTeleList.Contains(targetConfig) || targetConfig.teleportTarget == null || teleportTo == null) return; // 启动延迟传送协程 Coroutine delayCoro = StartCoroutine(DelayedSingleTeleport(targetConfig, configIndex)); delayedTeleportCoroutines.Add(delayCoro); } /// <summary> /// 单组配置的延迟传送协程(事件方式专用) /// </summary> private IEnumerator DelayedSingleTeleport(PickUpTele targetConfig, int configIndex) { float actualDelay = Mathf.Max(0f, targetConfig.teleportDelay); // 用配置索引区分日志(已移除configTag关联) Debug.Log($"[步骤{currentStepIndex + 1}-第{configIndex + 1}组] 事件触发:{actualDelay}秒后传送至 {targetConfig.teleportTarget.name}"); if (actualDelay > 0f) { yield return new WaitForSeconds(actualDelay); } // 执行对应传送 if (teleportTo != null && targetConfig.teleportTarget != null) { teleportTo.Teleport(targetConfig.teleportTarget); Debug.Log($"[步骤{currentStepIndex + 1}-第{configIndex + 1}组] 事件触发:已传送至 {targetConfig.teleportTarget.name}"); } } /// <summary> /// (外部调用)完成当前步骤 /// </summary> public void CompleteCurrentStep() { if (currentStepIndex < 0 || currentStepIndex >= steps.Length) return; TaskStep currentStep = steps[currentStepIndex]; // 1. 清理当前步骤的所有抓取传送配置 CleanupAllPickUpTele(); // 2. 停止步骤倒计时 if (countdownCoroutine != null) { StopCoroutine(countdownCoroutine); countdownCoroutine = null; } // 3. 执行步骤默认传送(与多抓取传送独立) if (currentStep.isTeleport && teleportTo != null && teleportTransform != null) { teleportTo.Teleport(teleportTransform.gameObject); Debug.Log($"[步骤{currentStepIndex + 1}] 执行步骤默认传送"); } // 4. 触发步骤结束事件与弹窗 currentStep.onStepEnd?.Invoke(); ShowCompletionPopup(); } /// <summary> /// 显示步骤完成提示弹窗 /// </summary> private void ShowCompletionPopup() { if (currentPopup != null) Destroy(currentPopup); if (popupPrefab == null || mainCanvas == null) return; TaskStep currentStep = steps[currentStepIndex]; currentPopup = Instantiate(popupPrefab, mainCanvas.transform); TextPopupController popupController = currentPopup.GetComponent<TextPopupController>(); if (popupController != null) { popupController.SetText(currentStep.stepEndText); // 播放结束语音 if (currentStep.stepEndAudio != null && audioSource != null) { audioSource.Stop(); audioSource.PlayOneShot(currentStep.stepEndAudio); } // 处理弹窗倒计时 bool isLastStep = currentStepIndex == steps.Length - 1; StartCoroutine(HandlePopupAfterAudio(popupController, isLastStep)); } } /// <summary> /// 处理弹窗语音播放与倒计时逻辑 /// </summary> private IEnumerator HandlePopupAfterAudio(TextPopupController popupController, bool isLastStep) { TaskStep currentStep = steps[currentStepIndex]; // 等待语音播放(提前4秒开始倒计时,避免语音未结束弹窗消失) if (currentStep.stepEndAudio != null) { float waitTime = Mathf.Max(0f, currentStep.stepEndAudio.length - 4f); yield return new WaitForSeconds(waitTime); } // 弹窗倒计时回调 Action onCountdownComplete = () => { Destroy(currentPopup); if (isLastStep) ShowFinalOverWindow(); else StartNextStep(); }; popupController.StartCountdown(stepOverTime, onCountdownComplete); } /// <summary> /// 显示任务最终结束窗口 /// </summary> private void ShowFinalOverWindow() { if (FinalOver != null && mainCanvas != null) { FinalOver.SetActive(true); Debug.Log("任务全部完成,显示结束窗口"); } } /// <summary> /// 跳转到指定步骤 /// </summary> public void JumpToStep(int stepIndex) { // 验证步骤索引有效性 if (stepIndex < 0 || stepIndex >= steps.Length || stepIndex == currentStepIndex) return; // 1. 清理当前步骤的所有抓取传送配置 CleanupAllPickUpTele(); // 2. 停止当前倒计时 if (countdownCoroutine != null) { StopCoroutine(countdownCoroutine); countdownCoroutine = null; } // 3. 结束当前步骤 if (currentStepIndex >= 0 && currentStepIndex < steps.Length) { steps[currentStepIndex].onStepEnd?.Invoke(); } // 4. 销毁现有弹窗 if (currentPopup != null) { Destroy(currentPopup); currentPopup = null; } // 5. 执行目标步骤 currentStepIndex = stepIndex; ExecuteCurrentStep(); } /// <summary> /// 重置所有任务步骤 /// </summary> public void ResetAllSteps() { // 1. 清理所有抓取传送配置 CleanupAllPickUpTele(); // 2. 停止所有协程 StopAllCoroutines(); countdownCoroutine = null; // 3. 清理UI事件监听 if (stepStartController != null) { stepStartController.OnAnimationComplete -= HandleAnimationComplete; stepStartController.OnShowAnimationComplete -= HandleShowAnimationComplete; } // 4. 结束当前步骤 if (currentStepIndex >= 0 && currentStepIndex < steps.Length) { steps[currentStepIndex].onStepEnd?.Invoke(); } // 5. 销毁弹窗与重置状态 if (currentPopup != null) Destroy(currentPopup); currentStepIndex = -1; FinalOver?.SetActive(false); // 隐藏结束窗口 // 6. 初始化音频源与UI if (audioSource == null) audioSource = gameObject.AddComponent<AudioSource>(); if (stepManager != null) stepManager.InitializeUI(this); // 7. 重新注册UI事件 if (stepStartController != null) { stepStartController.OnAnimationComplete += HandleAnimationComplete; stepStartController.OnShowAnimationComplete += HandleShowAnimationComplete; } Debug.Log("任务步骤已全部重置"); } } using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class StepManager : MonoBehaviour { // 步骤UI元素的封装类 [System.Serializable] public class StepUIElements { public Image defaultFrame; // 黑色默认框 public Image highlightFrame; // 绿色高亮框 public Image defaultArrow; // 灰色默认箭头 public Image highlightArrow; // 黄色高亮箭头 public TMP_Text stepText; // 步骤描述文本 } [Header("外部脚本引用")] [SerializeField] private AutoScrollbarVisibility scrollbarController; [SerializeField] private TaskController taskController; [SerializeField] private StepStartController stepStartController; [Header("步骤UI设置")] public GameObject stepPrefab; // 步骤UI预制体 public Transform stepsContainer; // 步骤UI的父容器 public TMP_Text stepTipsText; // 步骤提示文本组件 [Header("布局设置")] public bool useVerticalLayout = true; // 是否使用垂直布局 public int maxStepsPerColumn = 10; // 每列最大步骤数 public float cellWidth = 236f; // 单个步骤宽度 public float cellHeight = 70f; // 单个步骤高度 public float spacingX = 20f; // 水平间距 public float spacingY = 30f; // 垂直间距 public TextAnchor childAlignment = TextAnchor.MiddleCenter; // 对齐方式 public RectTransform containerRect; [Header("文本颜色设置")] public Color defaultTextColor = Color.white; // 默认文本颜色 public Color highlightedTextColor = Color.black; // 高亮文本颜色 // 运行时数据 private List<StepUIElements> uiElements = new List<StepUIElements>(); private List<RectTransform> stepTransforms = new List<RectTransform>(); /// <summary> /// 初始化UI /// </summary> public void InitializeUI(TaskController controller) { taskController = controller; CreateStepUI(); ArrangeSteps(); } /// <summary> /// 创建步骤UI /// </summary> private void CreateStepUI() { // 清除现有步骤 foreach (Transform child in stepsContainer) Destroy(child.gameObject); uiElements.Clear(); stepTransforms.Clear(); if (taskController == null) return; // 创建新步骤 for (int i = taskController.TotalSteps - 1; i >= 0; i--) { GameObject stepInstance = Instantiate(stepPrefab, stepsContainer); stepInstance.name = $"Step_{i + 1}"; // 添加或获取按钮组件 Button stepButton = stepInstance.GetComponent<Button>(); if (stepButton == null) { stepButton = stepInstance.AddComponent<Button>(); } // 设置按钮点击事件 int stepIndex = i; // 创建闭包变量 stepButton.onClick.AddListener(() => { taskController.JumpToStep(stepIndex); stepStartController.ShowUIStart(); }); // 配置按钮视觉 ColorBlock colors = stepButton.colors; colors.normalColor = new Color(1, 1, 1, 0); // 完全透明 colors.highlightedColor = new Color(1, 1, 1, 0.1f); // 轻微高亮 colors.pressedColor = new Color(1, 1, 1, 0.2f); // 按下效果 stepButton.colors = colors; RectTransform rect = stepInstance.GetComponent<RectTransform>(); rect.sizeDelta = new Vector2(cellWidth, cellHeight); stepTransforms.Add(rect); StepUIElements ui = new StepUIElements { defaultFrame = stepInstance.transform.Find("黑色默认框").GetComponent<Image>(), highlightFrame = stepInstance.transform.Find("绿色高亮框").GetComponent<Image>(), defaultArrow = stepInstance.transform.Find("默认箭头").GetComponent<Image>(), highlightArrow = stepInstance.transform.Find("高亮箭头").GetComponent<Image>(), stepText = stepInstance.GetComponentInChildren<TMP_Text>() }; // 设置步骤文本 if (ui.stepText != null && i < taskController.TotalSteps) { ui.stepText.text = $"{taskController.steps[i].name}"; } uiElements.Add(ui); } // 反转列表以保持逻辑顺序 uiElements.Reverse(); stepTransforms.Reverse(); } /// <summary> /// 更新步骤视觉状态 /// </summary> public void UpdateStepVisuals() { if (taskController == null) return; int stepsPerColumn = CalculateStepsPerColumn(); for (int i = 0; i < uiElements.Count; i++) { bool isCurrentStep = (i == taskController.CurrentStepIndex); bool isLastStep = (i == uiElements.Count - 1); bool isEndOfColumn = ((i + 1) % stepsPerColumn == 0); bool isLastInColumn = isEndOfColumn || isLastStep; StepUIElements step = uiElements[i]; // 框架显示逻辑 step.defaultFrame.gameObject.SetActive(!isCurrentStep); step.highlightFrame.gameObject.SetActive(isCurrentStep); // 箭头显示逻辑 bool showArrow = !isLastStep && !isLastInColumn; step.defaultArrow.gameObject.SetActive(showArrow && !isCurrentStep); step.highlightArrow.gameObject.SetActive(showArrow && isCurrentStep); // 文本颜色设置 if (step.stepText != null) { step.stepText.color = isCurrentStep ? highlightedTextColor : defaultTextColor; } } // 更新提示文本 stepTipsText.text = taskController.CurrentTip; // 更新滚动条 if (scrollbarController != null) scrollbarController.UpdateScrollbarVisibility(); } /// <summary> /// 网格布局排列算法(模拟Grid Layout Group)- 垂直方向 /// </summary> private void ArrangeSteps() { if (stepTransforms.Count == 0) return; if (useVerticalLayout) { ArrangeVertically(); } else { ArrangeHorizontally(); } } /// <summary> /// 水平排列 /// </summary> private void ArrangeHorizontally() { // 获取容器可用宽度 float containerWidth = containerRect.rect.width; // 计算每行可容纳的步骤数(考虑最大限制) int stepsPerRow = CalculateStepsPerRow(); // 计算总行数 int totalRows = Mathf.CeilToInt((float)stepTransforms.Count / stepsPerRow); // 计算内容总尺寸 float contentWidth = stepsPerRow * cellWidth + (stepsPerRow - 1) * spacingX; float contentHeight = totalRows * cellHeight + (totalRows - 1) * spacingY; // 计算对齐偏移量 - 确保居中 Vector2 alignmentOffset = CalculateCenteredAlignmentOffset(contentWidth, contentHeight, stepsPerRow, totalRows); // 排列所有步骤(从顶部开始) for (int i = 0; i < stepTransforms.Count; i++) { // 计算行列位置(左上到右下,水平排列) int row = i / stepsPerRow; int col = i % stepsPerRow; // 计算锚点位置(从顶部开始) float xPos = col * (cellWidth + spacingX) + alignmentOffset.x; float yPos = -row * (cellHeight + spacingY) - alignmentOffset.y; // 设置位置 stepTransforms[i].anchoredPosition = new Vector2(xPos, yPos); } } /// <summary> /// 垂直排列 /// </summary> private void ArrangeVertically() { // 获取容器可用高度 float containerHeight = containerRect.rect.height; // 计算每列可容纳的步骤数(考虑最大限制) int stepsPerColumn = CalculateStepsPerColumn(); // 计算总列数 int totalColumns = Mathf.CeilToInt((float)stepTransforms.Count / stepsPerColumn); // 计算内容总尺寸 float contentWidth = totalColumns * cellWidth + (totalColumns - 1) * spacingX; float contentHeight = stepsPerColumn * cellHeight + (stepsPerColumn - 1) * spacingY; // 计算对齐偏移量 - 确保居中 Vector2 alignmentOffset = CalculateCenteredAlignmentOffset(contentWidth, contentHeight, totalColumns, stepsPerColumn); // 排列所有步骤(从左到右,从上到下) for (int i = 0; i < stepTransforms.Count; i++) { // 计算行列位置(左上到右下,垂直排列) int column = i / stepsPerColumn; int row = i % stepsPerColumn; // 计算锚点位置 float xPos = column * (cellWidth + spacingX) + alignmentOffset.x; float yPos = -row * (cellHeight + spacingY) - alignmentOffset.y; // 设置位置 stepTransforms[i].anchoredPosition = new Vector2(xPos, yPos); } } /// <summary> /// 计算居中对齐偏移量 /// </summary> private Vector2 CalculateCenteredAlignmentOffset(float contentWidth, float contentHeight, int columns, int rows) { // 计算容器中心点 float containerCenterX = containerRect.rect.width / 2; float containerCenterY = -containerRect.rect.height / 2; // 计算内容区域中心点 float contentCenterX = contentWidth / 2; float contentCenterY = contentHeight / 2; // 计算偏移量,使内容区域中心与容器中心对齐 float xOffset = containerCenterX - contentCenterX; float yOffset = containerCenterY + contentCenterY; return new Vector2(xOffset, yOffset); } /// <summary> /// 计算每行步骤数 /// </summary> private int CalculateStepsPerRow() { // 计算可用宽度能容纳的步骤数 float availableWidth = containerRect.rect.width; int steps = Mathf.Max(1, Mathf.FloorToInt((availableWidth + spacingX) / (cellWidth + spacingX))); // 应用最大限制并返回 return Mathf.Min(steps, maxStepsPerColumn); } /// <summary> /// 计算每列步骤数 /// </summary> private int CalculateStepsPerColumn() { // 计算可用高度能容纳的步骤数 float availableHeight = containerRect.rect.height; int steps = Mathf.Max(1, Mathf.FloorToInt((availableHeight + spacingY) / (cellHeight + spacingY))); // 应用最大限制并返回 return Mathf.Min(steps, maxStepsPerColumn); } /// <summary> /// 获取中间步骤的索引 /// </summary> private int GetMiddleStepIndex() { return Mathf.FloorToInt(uiElements.Count / 2); } /// <summary> /// 确保中间步骤在视图中居中 /// </summary> public void EnsureMiddleStepCentered() { if (stepTransforms.Count == 0) return; int middleIndex = GetMiddleStepIndex(); RectTransform middleStep = stepTransforms[middleIndex]; // 计算中间步骤的位置 Vector2 middleStepPosition = middleStep.anchoredPosition; // 计算需要滚动的偏移量 Vector2 scrollOffset = new Vector2( -middleStepPosition.x + containerRect.rect.width / 2, -middleStepPosition.y - containerRect.rect.height / 2 ); // 应用滚动偏移到所有步骤 for (int i = 0; i < stepTransforms.Count; i++) { stepTransforms[i].anchoredPosition += scrollOffset; } } } 这三个脚本运用时,在编辑器中的Game窗口中,tipText是可以正常显示倒计时的 但是打包出来后安卓无法显示倒计时,且不仅仅是不显示Text,而是无倒计时
09-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值