拼图挑战:从基础到高级实现
本文介绍了Unity中拼图游戏的完整开发流程,从基础原理到高级实现。主要内容包括: 游戏概述与数学基础 拼图游戏通过移动碎片还原完整图像 基于排列组合理论,分析可达状态和可解性 提供检查拼图可解性的算法实现 核心设计与实现 图像切分与碎片生成机制 碎片移动交互逻辑 游戏状态验证与胜利判定系统 难度管理系统 进阶功能与优化 性能优化与内存管理策略 特效系统与视觉反馈 多语言本地化支持 自定义拼图导入
拼图挑战:从基础到高级实现
9.1 拼图游戏概述与原理
拼图游戏是一类经典的益智类游戏,玩家需要通过移动打乱的图片碎片,将其还原成完整的图像。这类游戏不仅能够锻炼玩家的观察力和空间思维能力,还能提供视觉上的满足感。拼图游戏通常采用方形网格布局,将一张完整的图片切分成若干小块,然后随机打乱这些碎片的位置,玩家通过移动碎片来还原原始图像。
拼图游戏的核心机制基于排列组合理论。对于一个n×n的拼图游戏(共有n²个格子),其可能的排列数量为n²!(n²的阶乘)。例如,一个3×3的拼图游戏有9!种可能的排列方式,大约为362880种。然而,值得注意的是,并非所有排列都能通过有效移动达到,这取决于游戏的具体规则。
在传统的滑块拼图(sliding puzzle)中,通常会有一个空白格子,玩家只能移动与空白格子相邻的碎片。这种约束使得可达状态的数量减少到n²!/2,因为只有偶排列(even permutations)才是可达的。
在Unity中实现拼图游戏时,我们需要处理以下几个关键要素:
- 图像的切分与碎片生成
- 碎片的随机打乱算法
- 碎片的移动机制
- 游戏胜利条件的判定
下面,我们将深入探讨这些核心概念,并通过代码实例展示如何在Unity中实现一个功能完整的拼图游戏。
9.2 拼图游戏数学基础与规则设计
拼图游戏背后有丰富的数学理论,理解这些理论对于设计出合理且有挑战性的游戏规则至关重要。
9.2.1 排列组合与可解性
在滑块拼图中,一个基本问题是:给定一个初始排列,是否存在一系列合法移动使其变为目标排列(通常是有序排列)。这个问题可以通过排列的奇偶性来解决。
在数学上,一个排列的奇偶性是通过计算其中的逆序对(inversion)数量来确定的。逆序对是指排列中的一对数字,其中前面的数字大于后面的数字。
对于n×n的拼图,如果n为奇数,则只有当初始排列的逆序对数量与空白格子的行号(从底部数起)之和为偶数时,该排列才是可解的。如果n为偶数,则当逆序对数量与空白格子的行列号之和为偶数时,该排列才是可解的。
下面是一个用于检查拼图是否可解的代码示例:
csharp
using System.Collections.Generic;
using UnityEngine;
public class PuzzleSolvability
{
/// <summary>
/// 检查给定的拼图状态是否可解
/// </summary>
/// <param name="puzzleState">当前拼图状态,一维数组表示,0代表空白格子</param>
/// <param name="gridSize">拼图的宽度/高度(假设是方形)</param>
/// <returns>如果拼图可解则返回true,否则返回false</returns>
public static bool IsSolvable(int[] puzzleState, int gridSize)
{
int inversionCount = CountInversions(puzzleState);
int blankRowFromBottom = GetBlankRowFromBottom(puzzleState, gridSize);
if (gridSize % 2 == 1)
{
// 奇数宽度的拼图
return inversionCount % 2 == 0;
}
else
{
// 偶数宽度的拼图
return (inversionCount + blankRowFromBottom) % 2 == 0;
}
}
/// <summary>
/// 计算排列中的逆序对数量
/// </summary>
/// <param name="puzzleState">拼图状态</param>
/// <returns>逆序对数量</returns>
private static int CountInversions(int[] puzzleState)
{
int inversionCount = 0;
for (int i = 0; i < puzzleState.Length; i++)
{
// 空白格子不参与逆序对计算
if (puzzleState[i] == 0)
continue;
for (int j = i + 1; j < puzzleState.Length; j++)
{
// 空白格子不参与逆序对计算
if (puzzleState[j] == 0)
continue;
if (puzzleState[i] > puzzleState[j])
{
inversionCount++;
}
}
}
return inversionCount;
}
/// <summary>
/// 获取空白格子距离底部的行数
/// </summary>
/// <param name="puzzleState">拼图状态</param>
/// <param name="gridSize">拼图的宽度/高度</param>
/// <returns>空白格子距离底部的行数(从1开始计数)</returns>
private static int GetBlankRowFromBottom(int[] puzzleState, int gridSize)
{
int blankIndex = System.Array.IndexOf(puzzleState, 0);
int blankRow = blankIndex / gridSize; // 从顶部计数的行号
return gridSize - blankRow; // 从底部计数的行号
}
/// <summary>
/// 生成一个可解的随机拼图状态
/// </summary>
/// <param name="gridSize">拼图的宽度/高度</param>
/// <returns>可解的随机拼图状态</returns>
public static int[] GenerateSolvablePuzzleState(int gridSize)
{
int[] targetState = new int[gridSize * gridSize];
// 生成目标状态(有序排列)
for (int i = 0; i < targetState.Length - 1; i++)
{
targetState[i] = i + 1;
}
targetState[targetState.Length - 1] = 0; // 空白格子
// 随机打乱状态
int[] shuffledState = ShuffleArray(targetState);
// 检查并确保可解性
while (!IsSolvable(shuffledState, gridSize))
{
// 如果不可解,交换两个非空白格子以改变奇偶性
SwapTwoNonBlankTiles(shuffledState);
}
return shuffledState;
}
/// <summary>
/// 随机打乱数组
/// </summary>
private static int[] ShuffleArray(int[] array)
{
int[] newArray = (int[])array.Clone();
for (int i = 0; i < newArray.Length; i++)
{
int randomIndex = Random.Range(i, newArray.Length);
int temp = newArray[i];
newArray[i] = newArray[randomIndex];
newArray[randomIndex] = temp;
}
return newArray;
}
/// <summary>
/// 交换两个非空白格子以改变排列的奇偶性
/// </summary>
private static void SwapTwoNonBlankTiles(int[] puzzleState)
{
// 找到两个非空白格子
int index1 = 0;
int index2 = 1;
// 确保它们不是空白格子
while (puzzleState[index1] == 0)
index1++;
while (index2 == index1 || puzzleState[index2] == 0)
index2++;
// 交换
int temp = puzzleState[index1];
puzzleState[index1] = puzzleState[index2];
puzzleState[index2] = temp;
}
}
这段代码提供了检查拼图可解性和生成可解拼图状态的功能,这对于确保游戏的公平性和可玩性至关重要。
9.2.2 拼图游戏的复杂度与难度设计
拼图游戏的难度主要由以下因素决定:
- 拼图的大小(n×n):更大的拼图通常更难
- 初始状态的混乱程度:逆序对数量越多,通常需要更多步骤来解决
- 图像的复杂性:具有明显特征和颜色差异的图像更容易拼
- 时间限制:增加时间压力会提高难度
在商业游戏中,通常会根据玩家技能水平提供不同难度级别。以下代码展示了如何根据难度设置拼图尺寸和混乱程度:
csharp
using UnityEngine;
public class PuzzleDifficultyManager
{
public enum DifficultyLevel
{
Easy,
Medium,
Hard,
Expert
}
/// <summary>
/// 根据难度级别获取拼图大小
/// </summary>
/// <param name="difficulty">难度级别</param>
/// <returns>拼图的网格大小</returns>
public static int GetPuzzleSize(DifficultyLevel difficulty)
{
switch (difficulty)
{
case DifficultyLevel.Easy:
return 3; // 3x3拼图
case DifficultyLevel.Medium:
return 4; // 4x4拼图
case DifficultyLevel.Hard:
return 5; // 5x5拼图
case DifficultyLevel.Expert:
return 6; // 6x6拼图
default:
return 4;
}
}
/// <summary>
/// 根据难度级别获取拼图的混乱程度(随机移动次数)
/// </summary>
/// <param name="difficulty">难度级别</param>
/// <returns>随机移动的次数</returns>
public static int GetShuffleMoves(DifficultyLevel difficulty)
{
switch (difficulty)
{
case DifficultyLevel.Easy:
return Random.Range(20, 30);
case DifficultyLevel.Medium:
return Random.Range(40, 60);
case DifficultyLevel.Hard:
return Random.Range(80, 100);
case DifficultyLevel.Expert:
return Random.Range(150, 200);
default:
return 50;
}
}
/// <summary>
/// 根据难度级别获取游戏时间限制(秒)
/// </summary>
/// <param name="difficulty">难度级别</param>
/// <returns>时间限制(秒),返回0表示无时间限制</returns>
public static float GetTimeLimit(DifficultyLevel difficulty)
{
switch (difficulty)
{
case DifficultyLevel.Easy:
return 0; // 无时间限制
case DifficultyLevel.Medium:
return 300; // 5分钟
case DifficultyLevel.Hard:
return 240; // 4分钟
case DifficultyLevel.Expert:
return 180; // 3分钟
default:
return 0;
}
}
}
这个难度管理系统为游戏提供了灵活的难度调整能力,可以根据玩家的反馈和游戏测试结果进行微调。
9.3 拼图游戏的核心设计与实现思路
9.3.1 原始图像与碎片生成机制
拼图游戏的第一步是将一张完整的图像切分成多个小块。在Unity中,我们可以使用Texture2D类的GetPixels和SetPixels方法来实现这一功能。
以下是一个图像切分器类,用于将原始图像切分成n×n个碎片:
csharp
using UnityEngine;
using System.Collections.Generic;
public class PuzzleTileGenerator
{
/// <summary>
/// 将原始图像切分成网格状的碎片
/// </summary>
/// <param name="originalTexture">原始图像纹理</param>
/// <param name="gridSize">网格大小(n×n)</param>
/// <param name="borderWidth">碎片边框宽度(像素)</param>
/// <param name="borderColor">边框颜色</param>
/// <returns>切分后的碎片纹理列表</returns>
public static List<Texture2D> SliceTextureIntoTiles(Texture2D originalTexture, int gridSize, int borderWidth = 1, Color borderColor = default)
{
// 如果未指定边框颜色,默认为黑色
if (borderColor == default)
borderColor = Color.black;
// 确保纹理可读
if (!originalTexture.isReadable)
{
Debug.LogError("Texture is not readable. Please enable Read/Write in import settings.");
return null;
}
List<Texture2D> tiles = new List<Texture2D>();
int tileWidth = originalTexture.width / gridSize;
int tileHeight = originalTexture.height / gridSize;
// 创建一个临时RenderTexture以确保我们获得完整的图像数据
RenderTexture tempRT = RenderTexture.GetTemporary(
originalTexture.width,
originalTexture.height,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Linear);
Graphics.Blit(originalTexture, tempRT);
// 保存当前的活动RenderTexture
RenderTexture previousRT = RenderTexture.active;
RenderTexture.active = tempRT;
// 创建一个新的纹理并读取像素
Texture2D readableTexture = new Texture2D(originalTexture.width, originalTexture.height);
readableTexture.ReadPixels(new Rect(0, 0, tempRT.width, tempRT.height), 0, 0);
readableTexture.Apply();
// 恢复之前的活动RenderTexture
RenderTexture.active = previousRT;
RenderTexture.ReleaseTemporary(tempRT);
// 切分纹理
for (int y = 0; y < gridSize; y++)
{
for (int x = 0; x < gridSize; x++)
{
// 计算切片区域
int startX = x * tileWidth;
int startY = y * tileHeight;
// 创建新纹理(包括边框)
Texture2D tileTexture = new Texture2D(tileWidth, tileHeight);
// 获取原始纹理中的像素
Color[] pixels = readableTexture.GetPixels(startX, startY, tileWidth, tileHeight);
// 如果需要边框,则添加边框
if (borderWidth > 0)
{
pixels = AddBorderToTile(pixels, tileWidth, tileHeight, borderWidth, borderColor);
}
// 设置纹理像素并应用
tileTexture.SetPixels(pixels);
tileTexture.Apply();
tiles.Add(tileTexture);
}
}
return tiles;
}
/// <summary>
/// 为碎片添加边框
/// </summary>
private static Color[] AddBorderToTile(Color[] originalPixels, int width, int height, int borderWidth, Color borderColor)
{
Color[] borderedPixels = new Color[width * height];
// 复制原始像素
System.Array.Copy(originalPixels, borderedPixels, originalPixels.Length);
// 添加顶部和底部边框
for (int x = 0; x < width; x++)
{
for (int b = 0; b < borderWidth; b++)
{
// 顶部边框
borderedPixels[x + b * width] = borderColor;
// 底部边框
borderedPixels[x + (height - 1 - b) * width] = borderColor;
}
}
// 添加左侧和右侧边框
for (int y = 0; y < height; y++)
{
for (int b = 0; b < borderWidth; b++)
{
// 左侧边框
borderedPixels[b + y * width] = borderColor;
// 右侧边框
borderedPixels[(width - 1 - b) + y * width] = borderColor;
}
}
return borderedPixels;
}
/// <summary>
/// 为切分后的碎片创建精灵对象
/// </summary>
/// <param name="tileTextures">碎片纹理列表</param>
/// <returns>碎片精灵列表</returns>
public static List<Sprite> CreateTileSprites(List<Texture2D> tileTextures)
{
List<Sprite> tileSprites = new List<Sprite>();
foreach (Texture2D texture in tileTextures)
{
Sprite sprite = Sprite.Create(
texture,
new Rect(0, 0, texture.width, texture.height),
new Vector2(0.5f, 0.5f), // 中心枢轴
100.0f // 像素/单位
);
tileSprites.Add(sprite);
}
return tileSprites;
}
}
这个类提供了将原始图像切分成网格状碎片的功能,并可以选择为每个碎片添加边框以便更清晰地区分。此外,它还提供了创建精灵对象的方法,这些精灵可以直接用于Unity的SpriteRenderer组件。
9.3.2 拼图碎片的移动与交互设计
在传统的滑块拼图中,玩家只能移动与空白格子相邻的碎片。以下是实现这一交互逻辑的代码:
csharp
using UnityEngine;
using System.Collections.Generic;
public class PuzzleTileController : MonoBehaviour
{
// 拼图的行列数
private int gridSize;
// 拼图碎片游戏对象数组
private GameObject[,] tiles;
// 空白格位置
private Vector2Int emptyTilePosition;
// 是否允许移动
private bool allowMovement = true;
// 移动动画持续时间
[SerializeField] private float moveDuration = 0.2f;
// 音效
[SerializeField] private AudioClip tileMoveSfx;
private AudioSource audioSource;
// 移动计数器
private int moveCount = 0;
// 移动完成事件
public delegate void MoveCompletedHandler(int newMoveCount);
public event MoveCompletedHandler OnMoveCompleted;
// 初始化
public void Initialize(int size, GameObject[,] puzzleTiles, Vector2Int emptyPos)
{
gridSize = size;
tiles = puzzleTiles;
emptyTilePosition = emptyPos;
// 添加音频源组件
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.volume = 0.5f;
// 重置移动计数器
moveCount = 0;
}
// 处理碎片点击
public void HandleTileClick(int x, int y)
{
if (!allowMovement)
return;
// 检查点击的碎片是否与空白格相邻
if (IsAdjacentToEmptyTile(x, y))
{
// 移动碎片
MoveTile(x, y);
}
}
// 检查碎片是否与空白格相邻
private bool IsAdjacentToEmptyTile(int x, int y)
{
// 计算与空白格的曼哈顿距离
int manhattanDistance = Mathf.Abs(x - emptyTilePosition.x) + Mathf.Abs(y - emptyTilePosition.y);
// 如果曼哈顿距离为1,说明相邻
return manhattanDistance == 1;
}
// 移动碎片
private void MoveTile(int x, int y)
{
// 暂时禁用移动
allowMovement = false;
// 获取要移动的碎片
GameObject tileToMove = tiles[x, y];
// 计算目标位置(空白格的世界坐标)
Vector3 targetPosition = CalculateWorldPosition(emptyTilePosition.x, emptyTilePosition.y);
// 播放移动音效
if (tileMoveSfx != null && audioSource != null)
{
audioSource.PlayOneShot(tileMoveSfx);
}
// 启动协程进行平滑移动
StartCoroutine(SmoothMoveTile(tileToMove, targetPosition, () =>
{
// 移动完成后更新数据结构
tiles[emptyTilePosition.x, emptyTilePosition.y] = tileToMove;
tiles[x, y] = null;
// 更新空白格位置
emptyTilePosition = new Vector2Int(x, y);
// 增加移动计数
moveCount++;
// 触发移动完成事件
OnMoveCompleted?.Invoke(moveCount);
// 重新启用移动
allowMovement = true;
}));
}
// 平滑移动协程
private System.Collections.IEnumerator SmoothMoveTile(GameObject tile, Vector3 targetPosition, System.Action onComplete)
{
Vector3 startPosition = tile.transform.position;
float elapsedTime = 0;
while (elapsedTime < moveDuration)
{
// 计算插值因子
float t = elapsedTime / moveDuration;
// 使用平滑插值
t = Mathf.SmoothStep(0, 1, t);
// 更新位置
tile.transform.position = Vector3.Lerp(startPosition, targetPosition, t);
// 更新时间
elapsedTime += Time.deltaTime;
yield return null;
}
// 确保最终位置精确
tile.transform.position = targetPosition;
// 调用完成回调
onComplete?.Invoke();
}
// 计算网格坐标对应的世界坐标
private Vector3 CalculateWorldPosition(int x, int y)
{
// 这个方法需要根据具体的游戏场景布局来实现
// 下面是一个简单的示例,假设拼图中心在世界坐标(0,0,0),且每个碎片尺寸为1单位
float offset = (gridSize - 1) * 0.5f; // 使拼图居中
float xPos = x - offset;
float yPos = offset - y; // Y轴反转使(0,0)位于左上角
return new Vector3(xPos, yPos, 0);
}
// 获取当前移动次数
public int GetMoveCount()
{
return moveCount;
}
// 检查是否可以移动指定位置的碎片
public bool CanMoveTile(int x, int y)
{
return IsAdjacentToEmptyTile(x, y);
}
// 获取空白格位置
public Vector2Int GetEmptyTilePosition()
{
return emptyTilePosition;
}
// 自动求解一步(用于提示功能)
public bool AutoSolveStep()
{
// 此方法可以使用求解算法来确定最佳移动
// 简单起见,这里只实现一个随机移动相邻碎片的逻辑
List<Vector2Int> adjacentPositions = new List<Vector2Int>();
// 检查上方碎片
if (emptyTilePosition.y > 0)
adjacentPositions.Add(new Vector2Int(emptyTilePosition.x, emptyTilePosition.y - 1));
// 检查下方碎片
if (emptyTilePosition.y < gridSize - 1)
adjacentPositions.Add(new Vector2Int(emptyTilePosition.x, emptyTilePosition.y + 1));
// 检查左侧碎片
if (emptyTilePosition.x > 0)
adjacentPositions.Add(new Vector2Int(emptyTilePosition.x - 1, emptyTilePosition.y));
// 检查右侧碎片
if (emptyTilePosition.x < gridSize - 1)
adjacentPositions.Add(new Vector2Int(emptyTilePosition.x + 1, emptyTilePosition.y));
// 如果有相邻碎片
if (adjacentPositions.Count > 0)
{
// 随机选择一个
int randomIndex = Random.Range(0, adjacentPositions.Count);
Vector2Int tilePos = adjacentPositions[randomIndex];
// 移动该碎片
HandleTileClick(tilePos.x, tilePos.y);
return true;
}
return false;
}
}
这个控制器类处理拼图碎片的点击事件和移动逻辑。它确保只有与空白格相邻的碎片才能移动,并提供平滑的移动动画和音效反馈。此外,它还维护一个移动计数器,可用于评分和游戏统计。
9.3.3 游戏状态判断与正确性验证
拼图游戏的核心目标是将打乱的碎片还原为完整的图像。为了判断玩家是否成功完成拼图,我们需要实现状态验证机制:
csharp
using UnityEngine;
public class PuzzleStateValidator
{
/// <summary>
/// 检查当前拼图状态是否已解决
/// </summary>
/// <param name="currentState">当前拼图状态,一维数组表示</param>
/// <param name="targetState">目标状态,通常是有序排列</param>
/// <returns>如果拼图已解决则返回true</returns>
public static bool IsPuzzleSolved(int[] currentState, int[] targetState)
{
if (currentState.Length != targetState.Length)
return false;
for (int i = 0; i < currentState.Length; i++)
{
if (currentState[i] != targetState[i])
return false;
}
return true;
}
/// <summary>
/// 获取默认的目标状态(有序排列)
/// </summary>
/// <param name="gridSize">拼图大小</param>
/// <returns>目标状态数组</returns>
public static int[] GetDefaultTargetState(int gridSize)
{
int[] targetState = new int[gridSize * gridSize];
for (int i = 0; i < targetState.Length - 1; i++)
{
targetState[i] = i + 1;
}
// 空白格在右下角
targetState[targetState.Length - 1] = 0;
return targetState;
}
/// <summary>
/// 计算当前状态与目标状态的差异程度(曼哈顿距离总和)
/// </summary>
/// <param name="currentState">当前拼图状态</param>
/// <param name="gridSize">拼图大小</param>
/// <returns>曼哈顿距离总和</returns>
public static int CalculateTotalManhattanDistance(int[] currentState, int gridSize)
{
int totalDistance = 0;
for (int i = 0; i < currentState.Length; i++)
{
int value = currentState[i];
// 空白格不计算距离
if (value == 0)
continue;
// 计算当前位置
int currentX = i % gridSize;
int currentY = i / gridSize;
// 计算目标位置(对于值为value的碎片)
int targetIndex = value - 1;
int targetX = targetIndex % gridSize;
int targetY = targetIndex / gridSize;
// 计算曼哈顿距离并累加
int distance = Mathf.Abs(currentX - targetX) + Mathf.Abs(currentY - targetY);
totalDistance += distance;
}
return totalDistance;
}
/// <summary>
/// 计算当前状态的完成百分比
/// </summary>
/// <param name="currentState">当前拼图状态</param>
/// <param name="gridSize">拼图大小</param>
/// <returns>完成百分比(0-100)</returns>
public static float CalculateCompletionPercentage(int[] currentState, int gridSize)
{
int correctTiles = 0;
int[] targetState = GetDefaultTargetState(gridSize);
for (int i = 0; i < currentState.Length; i++)
{
if (currentState[i] == targetState[i])
correctTiles++;
}
return (float)correctTiles / currentState.Length * 100f;
}
}
这个验证器类提供了检查拼图是否已解决的功能,并可以计算完成百分比作为游戏进度指标。此外,它还实现了计算曼哈顿距离总和的方法,这可以用于评估拼图状态距离目标状态的"距离",从而为玩家提供解题进度反馈。
9.3.4 游戏胜利判定与奖励机制
当玩家成功完成拼图时,游戏应当提供适当的反馈和奖励。以下是实现胜利判定和奖励机制的代码:
csharp
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class PuzzleGameManager : MonoBehaviour
{
// 拼图数据
private int gridSize;
private int[] currentState;
private int[] targetState;
// 界面引用
[SerializeField] private Text moveCountText;
[SerializeField] private Text timeText;
[SerializeField] private GameObject victoryPanel;
[SerializeField] private Text victoryMoveText;
[SerializeField] private Text victoryTimeText;
[SerializeField] private Text victoryStarText;
// 计时器
private float gameTime = 0f;
private bool isGameActive = false;
// 移动次数
private int moveCount = 0;
// 音效
[SerializeField] private AudioClip victorySfx;
private AudioSource audioSource;
// 特效
[SerializeField] private GameObject confettiEffectPrefab;
// 初始化
private void Start()
{
audioSource = GetComponent<AudioSource>();
if (audioSource == null)
audioSource = gameObject.AddComponent<AudioSource>();
// 默认隐藏胜利面板
if (victoryPanel != null)
victoryPanel.SetActive(false);
}
// 更新
private void Update()
{
if (isGameActive)
{
// 更新游戏时间
gameTime += Time.deltaTime;
// 更新UI
UpdateTimeUI();
}
}
// 初始化游戏
public void InitializeGame(int size, int[] initialState)
{
gridSize = size;
currentState = initialState;
targetState = PuzzleStateValidator.GetDefaultTargetState(size);
// 重置计时器和移动计数
gameTime = 0f;
moveCount = 0;
// 更新UI
UpdateMoveCountUI();
UpdateTimeUI();
// 开始游戏
isGameActive = true;
}
// 更新移动次数
public void UpdateMoveCount(int newMoveCount)
{
moveCount = newMoveCount;
UpdateMoveCountUI();
// 检查是否完成
CheckForVictory();
}
// 更新当前拼图状态
public void UpdateCurrentState(int[] newState)
{
currentState = newState;
// 检查是否完成
CheckForVictory();
}
// 检查是否获胜
private void CheckForVictory()
{
if (PuzzleStateValidator.IsPuzzleSolved(currentState, targetState))
{
// 游戏结束
GameVictory();
}
}
// 游戏胜利处理
private void GameVictory()
{
isGameActive = false;
// 播放胜利音效
if (victorySfx != null && audioSource != null)
{
audioSource.PlayOneShot(victorySfx);
}
// 显示胜利特效
ShowVictoryEffects();
// 显示胜利面板
ShowVictoryPanel();
// 解锁下一关卡
UnlockNextLevel();
// 保存成绩
SaveGameResults();
}
// 显示胜利特效
private void ShowVictoryEffects()
{
if (confettiEffectPrefab != null)
{
// 在屏幕中心生成粒子特效
GameObject confetti = Instantiate(confettiEffectPrefab, Vector3.zero, Quaternion.identity);
// 5秒后销毁特效
Destroy(confetti, 5f);
}
}
// 显示胜利面板
private void ShowVictoryPanel()
{
if (victoryPanel != null)
{
// 更新胜利面板信息
if (victoryMoveText != null)
victoryMoveText.text = "移动次数: " + moveCount;
if (victoryTimeText != null)
victoryTimeText.text = "用时: " + FormatTime(gameTime);
if (victoryStarText != null)
victoryStarText.text = "星级评价: " + CalculateStarRating();
// 显示面板
victoryPanel.SetActive(true);
}
}
// 计算星级评价
private int CalculateStarRating()
{
// 根据移动次数和时间计算星级
// 这个公式需要根据具体游戏设计调整
// 基准移动次数(理想情况)
int idealMoves = gridSize * gridSize * 2;
// 基准时间(秒)
float idealTime = gridSize * gridSize * 3;
// 计算移动得分(最高50分)
float moveScore = Mathf.Max(0, 50 - (moveCount - idealMoves) * 0.5f);
// 计算时间得分(最高50分)
float timeScore = Mathf.Max(0, 50 - (gameTime - idealTime) * 0.2f);
// 总得分
float totalScore = moveScore + timeScore;
// 转换为星级
if (totalScore >= 80)
return 3; // 3星
else if (totalScore >= 50)
return 2; // 2星
else
return 1; // 1星
}
// 解锁下一关卡
private void UnlockNextLevel()
{
// 获取当前关卡
int currentLevel = PlayerPrefs.GetInt("CurrentLevel", 0);
// 解锁下一关卡
PlayerPrefs.SetInt("UnlockedLevel", currentLevel + 1);
PlayerPrefs.Save();
}
// 保存游戏成绩
private void SaveGameResults()
{
int currentLevel = PlayerPrefs.GetInt("CurrentLevel", 0);
// 保存最佳移动次数
string moveKey = "Level" + currentLevel + "BestMoves";
int bestMoves = PlayerPrefs.GetInt(moveKey, int.MaxValue);
if (moveCount < bestMoves)
{
PlayerPrefs.SetInt(moveKey, moveCount);
}
// 保存最佳时间
string timeKey = "Level" + currentLevel + "BestTime";
float bestTime = PlayerPrefs.GetFloat(timeKey, float.MaxValue);
if (gameTime < bestTime)
{
PlayerPrefs.SetFloat(timeKey, gameTime);
}
// 保存最高星级
string starKey = "Level" + currentLevel + "Stars";
int currentStars = CalculateStarRating();
int bestStars = PlayerPrefs.GetInt(starKey, 0);
if (currentStars > bestStars)
{
PlayerPrefs.SetInt(starKey, currentStars);
}
PlayerPrefs.Save();
}
// 更新移动次数UI
private void UpdateMoveCountUI()
{
if (moveCountText != null)
{
moveCountText.text = "移动: " + moveCount;
}
}
// 更新时间UI
private void UpdateTimeUI()
{
if (timeText != null)
{
timeText.text = "时间: " + FormatTime(gameTime);
}
}
// 格式化时间
private string FormatTime(float timeInSeconds)
{
int minutes = Mathf.FloorToInt(timeInSeconds / 60);
int seconds = Mathf.FloorToInt(timeInSeconds % 60);
return string.Format("{0:00}:{1:00}", minutes, seconds);
}
}
这个游戏管理器类负责跟踪游戏状态、计时、计数以及处理胜利逻辑。当玩家成功完成拼图时,它会显示胜利面板和特效,并保存游戏成绩。此外,它还实现了星级评价系统,根据玩家的表现(移动次数和时间)给予不同级别的奖励。
9.3.5 完整游戏流程与控制逻辑
下面是拼图游戏的核心流程和控制逻辑,将前面介绍的所有组件整合在一起:
csharp
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
public class PuzzleGame : MonoBehaviour
{
// 配置
[SerializeField] private Texture2D[] puzzleImages;
[SerializeField] private PuzzleDifficultyManager.DifficultyLevel difficulty = PuzzleDifficultyManager.DifficultyLevel.Medium;
// UI引用
[SerializeField] private Transform puzzleContainer;
[SerializeField] private Button restartButton;
[SerializeField] private Button hintButton;
[SerializeField] private Button menuButton;
[SerializeField] private Image previewImage;
[SerializeField] private Slider difficultySlider;
// 组件引用
private PuzzleTileController tileController;
private PuzzleGameManager gameManager;
// 拼图数据
private int gridSize;
private GameObject[,] tilePieces;
private int[] puzzleState;
private Vector2Int emptyTilePos;
private Texture2D currentPuzzleImage;
// 预制件
[SerializeField] private GameObject tilePrefab;
// 初始化
private void Start()
{
// 获取组件
tileController = GetComponent<PuzzleTileController>();
gameManager = GetComponent<PuzzleGameManager>();
if (tileController == null)
tileController = gameObject.AddComponent<PuzzleTileController>();
if (gameManager == null)
gameManager = gameObject.AddComponent<PuzzleGameManager>();
// 设置按钮监听
if (restartButton != null)
restartButton.onClick.AddListener(RestartGame);
if (hintButton != null)
hintButton.onClick.AddListener(ShowHint);
if (menuButton != null)
menuButton.onClick.AddListener(ReturnToMenu);
if (difficultySlider != null)
{
difficultySlider.minValue = 0;
difficultySlider.maxValue = System.Enum.GetValues(typeof(PuzzleDifficultyManager.DifficultyLevel)).Length - 1;
difficultySlider.value = (int)difficulty;
difficultySlider.onValueChanged.AddListener(OnDifficultyChanged);
}
// 启动游戏
StartGame();
}
// 启动游戏
private void StartGame()
{
// 清理现有拼图
ClearPuzzle();
// 根据难度确定拼图大小
gridSize = PuzzleDifficultyManager.GetPuzzleSize(difficulty);
// 选择一张随机图片
if (puzzleImages != null && puzzleImages.Length > 0)
{
int randomIndex = Random.Range(0, puzzleImages.Length);
currentPuzzleImage = puzzleImages[randomIndex];
// 更新预览图
if (previewImage != null)
{
previewImage.sprite = Sprite.Create(
currentPuzzleImage,
new Rect(0, 0, currentPuzzleImage.width, currentPuzzleImage.height),
new Vector2(0.5f, 0.5f)
);
}
}
else
{
Debug.LogError("No puzzle images assigned!");
return;
}
// 创建拼图碎片
CreatePuzzleTiles();
// 打乱拼图
ShufflePuzzle();
// 初始化控制器
tileController.Initialize(gridSize, tilePieces, emptyTilePos);
// 注册移动完成事件
tileController.OnMoveCompleted += OnTileMoved;
// 初始化游戏管理器
gameManager.InitializeGame(gridSize, puzzleState);
}
// 创建拼图碎片
private void CreatePuzzleTiles()
{
tilePieces = new GameObject[gridSize, gridSize];
puzzleState = new int[gridSize * gridSize];
// 切分图像
List<Texture2D> tileTextures = PuzzleTileGenerator.SliceTextureIntoTiles(currentPuzzleImage, gridSize);
List<Sprite> tileSprites = PuzzleTileGenerator.CreateTileSprites(tileTextures);
// 计算碎片大小
float tileSize = 1.0f;
float puzzleSize = gridSize * tileSize;
float startX = -puzzleSize / 2 + tileSize / 2;
float startY = puzzleSize / 2 - tileSize / 2;
// 创建碎片
int tileIndex = 0;
for (int y = 0; y < gridSize; y++)
{
for (int x = 0; x < gridSize; x++)
{
// 计算位置
Vector3 position = new Vector3(
startX + x * tileSize,
startY - y * tileSize,
0
);
// 最后一个位置是空白格
if (x == gridSize - 1 && y == gridSize - 1)
{
// 记录空白格位置
emptyTilePos = new Vector2Int(x, y);
puzzleState[tileIndex] = 0; // 空白格标记为0
}
else
{
// 创建碎片
GameObject tile = Instantiate(tilePrefab, position, Quaternion.identity, puzzleContainer);
// 设置精灵
SpriteRenderer renderer = tile.GetComponent<SpriteRenderer>();
if (renderer != null)
{
renderer.sprite = tileSprites[tileIndex];
}
// 设置名称
tile.name = "Tile_" + (tileIndex + 1);
// 添加点击处理
int capturedX = x;
int capturedY = y;
TileClickHandler clickHandler = tile.GetComponent<TileClickHandler>();
if (clickHandler == null)
clickHandler = tile.AddComponent<TileClickHandler>();
clickHandler.Initialize(() => tileController.HandleTileClick(capturedX, capturedY));
// 存储引用
tilePieces[x, y] = tile;
// 记录状态(索引从1开始)
puzzleState[tileIndex] = tileIndex + 1;
}
tileIndex++;
}
}
}
// 打乱拼图
private void ShufflePuzzle()
{
int shuffleMoves = PuzzleDifficultyManager.GetShuffleMoves(difficulty);
// 从解决状态开始,执行随机移动来打乱拼图
for (int i = 0; i < shuffleMoves; i++)
{
// 获取空白格周围的可移动碎片
List<Vector2Int> validMoves = new List<Vector2Int>();
// 上方碎片
if (emptyTilePos.y > 0)
validMoves.Add(new Vector2Int(emptyTilePos.x, emptyTilePos.y - 1));
// 下方碎片
if (emptyTilePos.y < gridSize - 1)
validMoves.Add(new Vector2Int(emptyTilePos.x, emptyTilePos.y + 1));
// 左侧碎片
if (emptyTilePos.x > 0)
validMoves.Add(new Vector2Int(emptyTilePos.x - 1, emptyTilePos.y));
// 右侧碎片
if (emptyTilePos.x < gridSize - 1)
validMoves.Add(new Vector2Int(emptyTilePos.x + 1, emptyTilePos.y));
// 随机选择一个移动
if (validMoves.Count > 0)
{
int randomIndex = Random.Range(0, validMoves.Count);
Vector2Int movePos = validMoves[randomIndex];
// 移动碎片
MoveTile(movePos.x, movePos.y, true);
}
}
// 确保拼图可解
if (!PuzzleSolvability.IsSolvable(puzzleState, gridSize))
{
// 如果不可解,交换两个非空白格子
int index1 = 0;
int index2 = 1;
// 确保它们不是空白格子
while (puzzleState[index1] == 0)
index1++;
while (index2 == index1 || puzzleState[index2] == 0)
index2++;
// 交换状态
int temp = puzzleState[index1];
puzzleState[index1] = puzzleState[index2];
puzzleState[index2] = temp;
// 计算对应的二维索引
int x1 = index1 % gridSize;
int y1 = index1 / gridSize;
int x2 = index2 % gridSize;
int y2 = index2 / gridSize;
// 交换碎片对象
GameObject tempTile = tilePieces[x1, y1];
tilePieces[x1, y1] = tilePieces[x2, y2];
tilePieces[x2, y2] = tempTile;
// 交换位置
if (tilePieces[x1, y1] != null && tilePieces[x2, y2] != null)
{
Vector3 tempPos = tilePieces[x1, y1].transform.position;
tilePieces[x1, y1].transform.position = tilePieces[x2, y2].transform.position;
tilePieces[x2, y2].transform.position = tempPos;
}
}
// 更新游戏管理器中的状态
gameManager.UpdateCurrentState(puzzleState);
}
// 移动碎片(用于打乱)
private void MoveTile(int x, int y, bool immediate)
{
// 获取当前碎片
GameObject tile = tilePieces[x, y];
if (tile == null)
return;
// 计算一维索引
int index1D = y * gridSize + x;
int emptyIndex1D = emptyTilePos.y * gridSize + emptyTilePos.x;
// 交换状态
puzzleState[emptyIndex1D] = puzzleState[index1D];
puzzleState[index1D] = 0;
// 交换对象引用
tilePieces[emptyTilePos.x, emptyTilePos.y] = tile;
tilePieces[x, y] = null;
// 如果是立即移动(用于打乱)
if (immediate)
{
// 计算目标位置
float tileSize = 1.0f;
float puzzleSize = gridSize * tileSize;
float startX = -puzzleSize / 2 + tileSize / 2;
float startY = puzzleSize / 2 - tileSize / 2;
Vector3 targetPosition = new Vector3(
startX + emptyTilePos.x * tileSize,
startY - emptyTilePos.y * tileSize,
0
);
tile.transform.position = targetPosition;
}
// 更新空白格位置
emptyTilePos = new Vector2Int(x, y);
}
// 碎片移动完成回调
private void OnTileMoved(int newMoveCount)
{
// 更新游戏管理器中的移动次数和状态
gameManager.UpdateMoveCount(newMoveCount);
gameManager.UpdateCurrentState(puzzleState);
}
// 清理拼图
private void ClearPuzzle()
{
if (puzzleContainer != null)
{
foreach (Transform child in puzzleContainer)
{
Destroy(child.gameObject);
}
}
}
// 重启游戏
private void RestartGame()
{
StartGame();
}
// 显示提示
private void ShowHint()
{
// 自动移动一步
tileController.AutoSolveStep();
}
// 返回菜单
private void ReturnToMenu()
{
// 加载菜单场景
SceneManager.LoadScene("MainMenu");
}
// 难度变更回调
private void OnDifficultyChanged(float value)
{
difficulty = (PuzzleDifficultyManager.DifficultyLevel)Mathf.RoundToInt(value);
// 不立即重启游戏,等玩家点击重启按钮
}
}
// 碎片点击处理器
public class TileClickHandler : MonoBehaviour
{
private System.Action onClickCallback;
public void Initialize(System.Action callback)
{
onClickCallback = callback;
}
private void OnMouseDown()
{
onClickCallback?.Invoke();
}
}
这个PuzzleGame类是游戏的主控制器,它协调所有其他组件的工作。它负责创建拼图碎片、打乱拼图、处理玩家输入,以及管理游戏状态。此外,它还提供了重启游戏、显示提示和返回菜单等功能。
9.4 拼图游戏的实际实现与优化
9.4.1 项目准备与资源设置
在开始实现拼图游戏之前,我们需要准备好项目环境和必要的资源:
csharp
using UnityEngine;
using UnityEditor;
using System.IO;
#if UNITY_EDITOR
public class PuzzleGameSetup : EditorWindow
{
// 项目设置
private string projectName = "拼图挑战";
private string companyName = "我的游戏公司";
private string productVersion = "1.0.0";
// 文件夹结构
private readonly string[] folders = new string[]
{
"Assets/Scripts",
"Assets/Scripts/Core",
"Assets/Scripts/UI",
"Assets/Scripts/Utils",
"Assets/Prefabs",
"Assets/Textures",
"Assets/Textures/Puzzles",
"Assets/Textures/UI",
"Assets/Materials",
"Assets/Scenes",
"Assets/Audio",
"Assets/Audio/Music",
"Assets/Audio/SFX",
"Assets/Animations",
"Assets/Resources"
};
// 预制件设置
private GameObject tilePrefab;
private Texture2D[] puzzleImages;
[MenuItem("工具/拼图游戏/项目设置")]
public static void ShowWindow()
{
GetWindow<PuzzleGameSetup>("拼图游戏设置");
}
private void OnGUI()
{
GUILayout.Label("拼图游戏项目设置", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 项目信息
projectName = EditorGUILayout.TextField("项目名称", projectName);
companyName = EditorGUILayout.TextField("公司名称", companyName);
productVersion = EditorGUILayout.TextField("产品版本", productVersion);
EditorGUILayout.Space();
// 预制件设置
tilePrefab = (GameObject)EditorGUILayout.ObjectField("碎片预制件", tilePrefab, typeof(GameObject), false);
// 拼图图像
EditorGUILayout.LabelField("拼图图像");
ScriptableObject target = this;
SerializedObject so = new SerializedObject(target);
SerializedProperty imagesProperty = so.FindProperty("puzzleImages");
EditorGUILayout.PropertyField(imagesProperty, true);
so.ApplyModifiedProperties();
EditorGUILayout.Space();
// 应用设置按钮
if (GUILayout.Button("应用项目设置"))
{
ApplyProjectSettings();
}
if (GUILayout.Button("创建文件夹结构"))
{
CreateFolderStructure();
}
if (GUILayout.Button("创建默认场景"))
{
CreateDefaultScenes();
}
if (GUILayout.Button("导入默认资源"))
{
ImportDefaultAssets();
}
EditorGUILayout.Space();
EditorGUILayout.HelpBox("请确保已备份项目,操作可能会修改项目设置。", MessageType.Warning);
}
// 应用项目设置
private void ApplyProjectSettings()
{
// 设置产品名称
PlayerSettings.productName = projectName;
// 设置公司名称
PlayerSettings.companyName = companyName;
// 设置版本号
PlayerSettings.bundleVersion = productVersion;
// 设置目标平台
#if UNITY_2021_1_OR_NEWER
PlayerSettings.SetScriptingBackend(BuildTargetGroup.Standalone, ScriptingImplementation.Mono2x);
#else
PlayerSettings.SetScriptingBackend(BuildTargetGroup.Standalone, ScriptingImplementation.Mono2x);
#endif
// 设置分辨率
PlayerSettings.defaultScreenWidth = 1280;
PlayerSettings.defaultScreenHeight = 720;
PlayerSettings.defaultIsFullScreen = false;
// 设置图标
if (puzzleImages != null && puzzleImages.Length > 0)
{
Texture2D icon = puzzleImages[0];
PlayerSettings.SetIconsForTargetGroup(BuildTargetGroup.Unknown, new Texture2D[] { icon });
}
Debug.Log("已应用项目设置");
}
// 创建文件夹结构
private void CreateFolderStructure()
{
foreach (string folder in folders)
{
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
Debug.Log("创建文件夹: " + folder);
}
}
AssetDatabase.Refresh();
Debug.Log("文件夹结构创建完成");
}
// 创建默认场景
private void CreateDefaultScenes()
{
// 保存当前场景
if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
// 创建主菜单场景
EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects);
EditorSceneManager.SaveScene(EditorSceneManager.GetActiveScene(), "Assets/Scenes/MainMenu.unity");
// 创建游戏场景
EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects);
EditorSceneManager.SaveScene(EditorSceneManager.GetActiveScene(), "Assets/Scenes/GameScene.unity");
Debug.Log("默认场景创建完成");
}
}
// 导入默认资源
private void ImportDefaultAssets()
{
// 创建默认碎片预制件(如果未设置)
if (tilePrefab == null)
{
// 创建默认预制件
GameObject tileObj = new GameObject("TilePrefab");
// 添加必要组件
tileObj.AddComponent<SpriteRenderer>();
tileObj.AddComponent<BoxCollider2D>();
// 保存为预制件
if (!Directory.Exists("Assets/Prefabs"))
{
Directory.CreateDirectory("Assets/Prefabs");
}
string prefabPath = "Assets/Prefabs/TilePrefab.prefab";
PrefabUtility.SaveAsPrefabAsset(tileObj, prefabPath);
DestroyImmediate(tileObj);
// 加载创建的预制件
tilePrefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
Debug.Log("默认碎片预制件创建完成");
}
// 导入示例拼图图像(如果未设置)
if (puzzleImages == null || puzzleImages.Length == 0)
{
// 这里应该从默认资源包导入图像
// 由于限制,这里只能示例如何实现
Debug.Log("请手动导入拼图图像");
}
AssetDatabase.Refresh();
}
}
#endif
这个编辑器工具提供了一个方便的界面来设置拼图游戏项目,包括创建文件夹结构、设置默认场景和导入必要的资源。这对于快速启动新项目或确保团队成员使用一致的项目结构非常有用。
9.4.2 游戏场景构建与UI设计
拼图游戏需要一个精心设计的UI来提供良好的用户体验。以下是构建游戏场景和UI的代码:
csharp
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class PuzzleGameUI : MonoBehaviour
{
// 场景引用
[SerializeField] private Transform puzzleContainer;
[SerializeField] private Transform uiContainer;
// UI预制件
[SerializeField] private GameObject buttonPrefab;
[SerializeField] private GameObject panelPrefab;
[SerializeField] private GameObject textPrefab;
// UI元素
[SerializeField] private Button restartButton;
[SerializeField] private Button hintButton;
[SerializeField] private Button menuButton;
[SerializeField] private Button pauseButton;
[SerializeField] private Image previewImage;
[SerializeField] private Slider difficultySlider;
[SerializeField] private TextMeshProUGUI moveCountText;
[SerializeField] private TextMeshProUGUI timeText;
[SerializeField] private GameObject victoryPanel;
// 事件回调
public System.Action onRestartClick;
public System.Action onHintClick;
public System.Action onMenuClick;
public System.Action onPauseClick;
public System.Action<float> onDifficultyChanged;
// 初始化
private void Awake()
{
SetupButtonListeners();
}
// 设置按钮监听
private void SetupButtonListeners()
{
if (restartButton != null)
restartButton.onClick.AddListener(() => onRestartClick?.Invoke());
if (hintButton != null)
hintButton.onClick.AddListener(() => onHintClick?.Invoke());
if (menuButton != null)
menuButton.onClick.AddListener(() => onMenuClick?.Invoke());
if (pauseButton != null)
pauseButton.onClick.AddListener(() => onPauseClick?.Invoke());
if (difficultySlider != null)
difficultySlider.onValueChanged.AddListener((value) => onDifficultyChanged?.Invoke(value));
}
// 创建游戏UI
public void CreateGameUI()
{
if (uiContainer == null)
{
// 创建UI容器
GameObject uiContainerObj = new GameObject("UI_Container");
uiContainer = uiContainerObj.transform;
// 添加Canvas组件
Canvas canvas = uiContainerObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
// 添加CanvasScaler组件
CanvasScaler scaler = uiContainerObj.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.matchWidthOrHeight = 0.5f;
// 添加GraphicRaycaster组件
uiContainerObj.AddComponent<GraphicRaycaster>();
}
// 创建顶部面板
GameObject topPanel = CreatePanel("TopPanel", new Vector2(0, 460), new Vector2(1800, 120));
topPanel.transform.SetParent(uiContainer, false);
// 创建移动次数文本
moveCountText = CreateText("MoveCountText", "移动: 0").GetComponent<TextMeshProUGUI>();
moveCountText.transform.SetParent(topPanel.transform, false);
moveCountText.rectTransform.anchoredPosition = new Vector2(-700, 0);
// 创建时间文本
timeText = CreateText("TimeText", "时间: 00:00").GetComponent<TextMeshProUGUI>();
timeText.transform.SetParent(topPanel.transform, false);
timeText.rectTransform.anchoredPosition = new Vector2(700, 0);
// 创建底部面板
GameObject bottomPanel = CreatePanel("BottomPanel", new Vector2(0, -460), new Vector2(1800, 120));
bottomPanel.transform.SetParent(uiContainer, false);
// 创建按钮
restartButton = CreateButton("RestartButton", "重新开始", new Vector2(-600, 0));
restartButton.transform.SetParent(bottomPanel.transform, false);
hintButton = CreateButton("HintButton", "提示", new Vector2(-200, 0));
hintButton.transform.SetParent(bottomPanel.transform, false);
pauseButton = CreateButton("PauseButton", "暂停", new Vector2(200, 0));
pauseButton.transform.SetParent(bottomPanel.transform, false);
menuButton = CreateButton("MenuButton", "菜单", new Vector2(600, 0));
menuButton.transform.SetParent(bottomPanel.transform, false);
// 创建右侧面板
GameObject rightPanel = CreatePanel("RightPanel", new Vector2(800, 0), new Vector2(300, 800));
rightPanel.transform.SetParent(uiContainer, false);
// 创建预览图
GameObject previewObj = new GameObject("PreviewImage");
previewObj.transform.SetParent(rightPanel.transform, false);
previewImage = previewObj.AddComponent<Image>();
previewImage.rectTransform.anchoredPosition = new Vector2(0, 200);
previewImage.rectTransform.sizeDelta = new Vector2(250, 250);
// 创建难度滑块
GameObject sliderObj = new GameObject("DifficultySlider");
sliderObj.transform.SetParent(rightPanel.transform, false);
difficultySlider = sliderObj.AddComponent<Slider>();
difficultySlider.rectTransform.anchoredPosition = new Vector2(0, -200);
difficultySlider.rectTransform.sizeDelta = new Vector2(250, 30);
// 设置滑块外观
GameObject background = new GameObject("Background");
background.transform.SetParent(sliderObj.transform, false);
Image bgImage = background.AddComponent<Image>();
bgImage.color = new Color(0.2f, 0.2f, 0.2f);
bgImage.rectTransform.anchorMin = Vector2.zero;
bgImage.rectTransform.anchorMax = Vector2.one;
bgImage.rectTransform.sizeDelta = Vector2.zero;
GameObject fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(sliderObj.transform, false);
RectTransform fillRect = fillArea.AddComponent<RectTransform>();
fillRect.anchorMin = new Vector2(0, 0.25f);
fillRect.anchorMax = new Vector2(1, 0.75f);
fillRect.sizeDelta = new Vector2(-20, 0);
fillRect.anchoredPosition = new Vector2(-5, 0);
GameObject fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
Image fillImage = fill.AddComponent<Image>();
fillImage.color = new Color(0.0f, 0.7f, 1.0f);
fillRect = fill.GetComponent<RectTransform>();
fillRect.anchorMin = Vector2.zero;
fillRect.anchorMax = new Vector2(0.5f, 1);
fillRect.sizeDelta = Vector2.zero;
GameObject handleArea = new GameObject("Handle Slide Area");
handleArea.transform.SetParent(sliderObj.transform, false);
RectTransform handleRect = handleArea.AddComponent<RectTransform>();
handleRect.anchorMin = Vector2.zero;
handleRect.anchorMax = Vector2.one;
handleRect.sizeDelta = new Vector2(-20, 0);
handleRect.anchoredPosition = Vector2.zero;
GameObject handle = new GameObject("Handle");
handle.transform.SetParent(handleArea.transform, false);
Image handleImage = handle.AddComponent<Image>();
handleImage.color = new Color(1.0f, 1.0f, 1.0f);
handleRect = handle.GetComponent<RectTransform>();
handleRect.anchorMin = new Vector2(0.5f, 0);
handleRect.anchorMax = new Vector2(0.5f, 1);
handleRect.sizeDelta = new Vector2(20, 0);
handleRect.anchoredPosition = Vector2.zero;
// 设置滑块组件
difficultySlider.fillRect = fill.GetComponent<RectTransform>();
difficultySlider.handleRect = handle.GetComponent<RectTransform>();
difficultySlider.direction = Slider.Direction.LeftToRight;
difficultySlider.minValue = 0;
difficultySlider.maxValue = 3;
difficultySlider.value = 1;
difficultySlider.wholeNumbers = true;
// 创建难度文本
TextMeshProUGUI difficultyText = CreateText("DifficultyText", "难度").GetComponent<TextMeshProUGUI>();
difficultyText.transform.SetParent(rightPanel.transform, false);
difficultyText.rectTransform.anchoredPosition = new Vector2(0, -150);
// 创建胜利面板(默认隐藏)
victoryPanel = CreatePanel("VictoryPanel", Vector2.zero, new Vector2(800, 600));
victoryPanel.transform.SetParent(uiContainer, false);
// 设置胜利面板背景
Image victoryBg = victoryPanel.GetComponent<Image>();
victoryBg.color = new Color(0.2f, 0.2f, 0.2f, 0.9f);
// 创建胜利标题
TextMeshProUGUI victoryTitle = CreateText("VictoryTitle", "恭喜完成!").GetComponent<TextMeshProUGUI>();
victoryTitle.transform.SetParent(victoryPanel.transform, false);
victoryTitle.rectTransform.anchoredPosition = new Vector2(0, 200);
victoryTitle.fontSize = 48;
// 创建移动次数文本
TextMeshProUGUI victoryMoves = CreateText("VictoryMoves", "移动次数: 0").GetComponent<TextMeshProUGUI>();
victoryMoves.transform.SetParent(victoryPanel.transform, false);
victoryMoves.rectTransform.anchoredPosition = new Vector2(0, 100);
// 创建用时文本
TextMeshProUGUI victoryTime = CreateText("VictoryTime", "用时: 00:00").GetComponent<TextMeshProUGUI>();
victoryTime.transform.SetParent(victoryPanel.transform, false);
victoryTime.rectTransform.anchoredPosition = new Vector2(0, 50);
// 创建星级评价
TextMeshProUGUI victoryStar = CreateText("VictoryStar", "★★★").GetComponent<TextMeshProUGUI>();
victoryStar.transform.SetParent(victoryPanel.transform, false);
victoryStar.rectTransform.anchoredPosition = new Vector2(0, 0);
victoryStar.fontSize = 48;
victoryStar.color = Color.yellow;
// 创建按钮
Button nextButton = CreateButton("NextButton", "下一关", new Vector2(0, -100));
nextButton.transform.SetParent(victoryPanel.transform, false);
Button menuButton2 = CreateButton("MenuButton2", "回到菜单", new Vector2(0, -200));
menuButton2.transform.SetParent(victoryPanel.transform, false);
// 默认隐藏胜利面板
victoryPanel.SetActive(false);
// 设置按钮监听
SetupButtonListeners();
}
// 创建面板
private GameObject CreatePanel(string name, Vector2 position, Vector2 size)
{
GameObject panel = new GameObject(name);
panel.AddComponent<RectTransform>();
Image image = panel.AddComponent<Image>();
image.color = new Color(0.1f, 0.1f, 0.1f, 0.8f);
RectTransform rectTransform = panel.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.anchoredPosition = position;
rectTransform.sizeDelta = size;
return panel;
}
// 创建按钮
private Button CreateButton(string name, string text, Vector2 position)
{
GameObject button = new GameObject(name);
button.AddComponent<RectTransform>();
Image image = button.AddComponent<Image>();
image.color = new Color(0.3f, 0.3f, 0.3f);
Button buttonComponent = button.AddComponent<Button>();
// 设置按钮颜色
ColorBlock colors = buttonComponent.colors;
colors.normalColor = new Color(0.3f, 0.3f, 0.3f);
colors.highlightedColor = new Color(0.4f, 0.4f, 0.4f);
colors.pressedColor = new Color(0.2f, 0.2f, 0.2f);
buttonComponent.colors = colors;
// 设置按钮大小和位置
RectTransform rectTransform = button.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.sizeDelta = new Vector2(200, 60);
rectTransform.anchoredPosition = position;
// 添加文本
GameObject textObj = CreateText(name + "Text", text);
textObj.transform.SetParent(button.transform, false);
TextMeshProUGUI textComponent = textObj.GetComponent<TextMeshProUGUI>();
textComponent.alignment = TextAlignmentOptions.Center;
return buttonComponent;
}
// 创建文本
private GameObject CreateText(string name, string content)
{
GameObject text = new GameObject(name);
text.AddComponent<RectTransform>();
TextMeshProUGUI textComponent = text.AddComponent<TextMeshProUGUI>();
textComponent.text = content;
textComponent.fontSize = 24;
textComponent.alignment = TextAlignmentOptions.Center;
textComponent.color = Color.white;
// 设置文本大小
RectTransform rectTransform = text.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0, 0);
rectTransform.anchorMax = new Vector2(1, 1);
rectTransform.sizeDelta = Vector2.zero;
return text;
}
// 更新移动次数
public void UpdateMoveCount(int count)
{
if (moveCountText != null)
{
moveCountText.text = "移动: " + count;
}
}
// 更新时间
public void UpdateTime(float timeInSeconds)
{
if (timeText != null)
{
int minutes = Mathf.FloorToInt(timeInSeconds / 60);
int seconds = Mathf.FloorToInt(timeInSeconds % 60);
timeText.text = string.Format("时间: {0:00}:{1:00}", minutes, seconds);
}
}
// 显示胜利面板
public void ShowVictoryPanel(int moveCount, float time, int starCount)
{
if (victoryPanel != null)
{
// 更新胜利面板信息
TextMeshProUGUI movesText = victoryPanel.transform.Find("VictoryMoves").GetComponent<TextMeshProUGUI>();
if (movesText != null)
movesText.text = "移动次数: " + moveCount;
TextMeshProUGUI timeText = victoryPanel.transform.Find("VictoryTime").GetComponent<TextMeshProUGUI>();
if (timeText != null)
{
int minutes = Mathf.FloorToInt(time / 60);
int seconds = Mathf.FloorToInt(time % 60);
timeText.text = string.Format("用时: {0:00}:{1:00}", minutes, seconds);
}
TextMeshProUGUI starText = victoryPanel.transform.Find("VictoryStar").GetComponent<TextMeshProUGUI>();
if (starText != null)
{
string stars = "";
for (int i = 0; i < starCount; i++)
stars += "★";
for (int i = starCount; i < 3; i++)
stars += "☆";
starText.text = stars;
}
// 显示面板
victoryPanel.SetActive(true);
}
}
// 隐藏胜利面板
public void HideVictoryPanel()
{
if (victoryPanel != null)
{
victoryPanel.SetActive(false);
}
}
// 设置预览图
public void SetPreviewImage(Sprite sprite)
{
if (previewImage != null)
{
previewImage.sprite = sprite;
}
}
}
这个UI管理器提供了创建和管理拼图游戏界面的功能,包括移动计数器、计时器、难度滑块以及胜利面板。它使用组件化的方法来构建UI元素,便于维护和扩展。此外,它还提供了各种回调接口,使游戏逻辑可以响应UI事件。
9.4.3 碎片生成与管理系统
在拼图游戏中,碎片的生成和管理是核心功能。以下是一个更完整的碎片管理系统:
csharp
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class PuzzleTileManager : MonoBehaviour
{
// 图像设置
[SerializeField] private Texture2D puzzleImage;
[SerializeField] private int borderWidth = 1;
[SerializeField] private Color borderColor = Color.black;
// 拼图设置
[SerializeField] private int gridSize = 4;
[SerializeField] private float tileSize = 1.0f;
[SerializeField] private Transform puzzleContainer;
// 预制件
[SerializeField] private GameObject tilePrefab;
// 碎片数据
private GameObject[,] tiles;
private int[] puzzleState;
private Vector2Int emptyTilePosition;
// 公共属性
public int GridSize => gridSize;
public int[] CurrentState => puzzleState;
public Vector2Int EmptyPosition => emptyTilePosition;
// 事件
public delegate void TileMovedEventHandler(int fromIndex, int toIndex);
public event TileMovedEventHandler OnTileMoved;
/// <summary>
/// 初始化拼图
/// </summary>
/// <param name="image">拼图图像</param>
/// <param name="size">网格大小</param>
/// <param name="container">容器对象</param>
public void Initialize(Texture2D image, int size, Transform container = null)
{
// 更新属性
puzzleImage = image;
gridSize = size;
if (container != null)
puzzleContainer = container;
// 如果没有容器,创建一个
if (puzzleContainer == null)
{
GameObject containerObj = new GameObject("PuzzleContainer");
puzzleContainer = containerObj.transform;
}
// 清理现有拼图
ClearPuzzle();
// 创建新拼图
CreatePuzzleTiles();
}
/// <summary>
/// 创建拼图碎片
/// </summary>
private void CreatePuzzleTiles()
{
if (puzzleImage == null)
{
Debug.LogError("没有设置拼图图像!");
return;
}
// 初始化数据结构
tiles = new GameObject[gridSize, gridSize];
puzzleState = new int[gridSize * gridSize];
// 切分图像
List<Texture2D> tileTextures = PuzzleTileGenerator.SliceTextureIntoTiles(puzzleImage, gridSize, borderWidth, borderColor);
List<Sprite> tileSprites = PuzzleTileGenerator.CreateTileSprites(tileTextures);
// 计算拼图大小和起始位置
float puzzleDimension = gridSize * tileSize;
Vector3 startPosition = new Vector3(-puzzleDimension / 2 + tileSize / 2, puzzleDimension / 2 - tileSize / 2, 0);
// 创建碎片对象
int tileIndex = 0;
for (int y = 0; y < gridSize; y++)
{
for (int x = 0; x < gridSize; x++)
{
// 计算位置
Vector3 position = startPosition + new Vector3(x * tileSize, -y * tileSize, 0);
// 设置状态(索引从1开始,0表示空白格)
int state = tileIndex + 1;
// 最后一个位置是空白格
if (x == gridSize - 1 && y == gridSize - 1)
{
// 空白格不创建游戏对象
emptyTilePosition = new Vector2Int(x, y);
puzzleState[tileIndex] = 0; // 空白格标记为0
}
else
{
// 创建碎片对象
GameObject tile = CreateTileObject(position, tileSprites[tileIndex], state);
// 存储引用
tiles[x, y] = tile;
puzzleState[tileIndex] = state;
}
tileIndex++;
}
}
}
/// <summary>
/// 创建单个碎片对象
/// </summary>
private GameObject CreateTileObject(Vector3 position, Sprite sprite, int value)
{
// 如果没有预制件,创建一个简单的游戏对象
GameObject tile;
if (tilePrefab != null)
{
tile = Instantiate(tilePrefab, position, Quaternion.identity, puzzleContainer);
}
else
{
tile = new GameObject("Tile_" + value);
tile.transform.position = position;
tile.transform.SetParent(puzzleContainer);
// 添加精灵渲染器
tile.AddComponent<SpriteRenderer>();
// 添加碰撞器
BoxCollider2D collider = tile.AddComponent<BoxCollider2D>();
collider.size = new Vector2(tileSize * 0.9f, tileSize * 0.9f);
}
// 设置名称
tile.name = "Tile_" + value;
// 设置精灵
SpriteRenderer renderer = tile.GetComponent<SpriteRenderer>();
if (renderer != null)
{
renderer.sprite = sprite;
}
// 添加点击处理
TileClickHandler clickHandler = tile.GetComponent<TileClickHandler>();
if (clickHandler == null)
clickHandler = tile.AddComponent<TileClickHandler>();
int x = Mathf.RoundToInt((position.x - startPosition.x) / tileSize);
int y = Mathf.RoundToInt((startPosition.y - position.y) / tileSize);
Vector2Int tilePosition = new Vector2Int(x, y);
clickHandler.Initialize(() => HandleTileClick(tilePosition));
return tile;
}
/// <summary>
/// 处理碎片点击
/// </summary>
public void HandleTileClick(Vector2Int position)
{
// 检查点击的碎片是否与空白格相邻
if (IsAdjacentToEmptyTile(position))
{
// 移动碎片
MoveTile(position.x, position.y);
}
}
/// <summary>
/// 检查是否与空白格相邻
/// </summary>
private bool IsAdjacentToEmptyTile(Vector2Int position)
{
// 计算与空白格的曼哈顿距离
int manhattanDistance = Mathf.Abs(position.x - emptyTilePosition.x) + Mathf.Abs(position.y - emptyTilePosition.y);
// 如果曼哈顿距离为1,说明相邻
return manhattanDistance == 1;
}
/// <summary>
/// 移动碎片
/// </summary>
private void MoveTile(int x, int y)
{
// 获取要移动的碎片
GameObject tile = tiles[x, y];
if (tile == null)
return;
// 计算一维索引
int fromIndex = y * gridSize + x;
int toIndex = emptyTilePosition.y * gridSize + emptyTilePosition.x;
// 计算目标位置
Vector3 targetPosition = CalculateWorldPosition(emptyTilePosition.x, emptyTilePosition.y);
// 启动协程进行平滑移动
StartCoroutine(SmoothMoveTile(tile, targetPosition, () =>
{
// 更新数据结构
tiles[emptyTilePosition.x, emptyTilePosition.y] = tile;
tiles[x, y] = null;
// 交换状态
puzzleState[toIndex] = puzzleState[fromIndex];
puzzleState[fromIndex] = 0;
// 更新空白格位置
emptyTilePosition = new Vector2Int(x, y);
// 触发事件
OnTileMoved?.Invoke(fromIndex, toIndex);
}));
}
/// <summary>
/// 平滑移动协程
/// </summary>
private System.Collections.IEnumerator SmoothMoveTile(GameObject tile, Vector3 targetPosition, System.Action onComplete)
{
Vector3 startPosition = tile.transform.position;
float moveDuration = 0.2f; // 移动持续时间
float elapsedTime = 0;
while (elapsedTime < moveDuration)
{
// 计算插值因子
float t = elapsedTime / moveDuration;
// 使用平滑插值
t = Mathf.SmoothStep(0, 1, t);
// 更新位置
tile.transform.position = Vector3.Lerp(startPosition, targetPosition, t);
// 更新时间
elapsedTime += Time.deltaTime;
yield return null;
}
// 确保最终位置精确
tile.transform.position = targetPosition;
// 调用完成回调
onComplete?.Invoke();
}
/// <summary>
/// 计算网格坐标对应的世界坐标
/// </summary>
private Vector3 CalculateWorldPosition(int x, int y)
{
float puzzleDimension = gridSize * tileSize;
Vector3 startPosition = new Vector3(-puzzleDimension / 2 + tileSize / 2, puzzleDimension / 2 - tileSize / 2, 0);
return startPosition + new Vector3(x * tileSize, -y * tileSize, 0);
}
/// <summary>
/// 打乱拼图
/// </summary>
public void ShufflePuzzle(int shuffleMoves)
{
// 从解决状态开始,执行随机移动来打乱拼图
for (int i = 0; i < shuffleMoves; i++)
{
// 获取空白格周围的可移动碎片
List<Vector2Int> validMoves = GetValidMoves();
// 随机选择一个移动
if (validMoves.Count > 0)
{
int randomIndex = Random.Range(0, validMoves.Count);
Vector2Int movePos = validMoves[randomIndex];
// 移动碎片
MoveTileImmediate(movePos.x, movePos.y);
}
}
// 确保拼图可解
if (!PuzzleSolvability.IsSolvable(puzzleState, gridSize))
{
// 如果不可解,交换两个非空白格子
SwapTwoNonEmptyTiles();
}
}
/// <summary>
/// 获取当前可进行的有效移动
/// </summary>
private List<Vector2Int> GetValidMoves()
{
List<Vector2Int> validMoves = new List<Vector2Int>();
// 检查上方碎片
if (emptyTilePosition.y > 0)
validMoves.Add(new Vector2Int(emptyTilePosition.x, emptyTilePosition.y - 1));
// 检查下方碎片
if (emptyTilePosition.y < gridSize - 1)
validMoves.Add(new Vector2Int(emptyTilePosition.x, emptyTilePosition.y + 1));
// 检查左侧碎片
if (emptyTilePosition.x > 0)
validMoves.Add(new Vector2Int(emptyTilePosition.x - 1, emptyTilePosition.y));
// 检查右侧碎片
if (emptyTilePosition.x < gridSize - 1)
validMoves.Add(new Vector2Int(emptyTilePosition.x + 1, emptyTilePosition.y));
return validMoves;
}
/// <summary>
/// 立即移动碎片(不带动画,用于打乱)
/// </summary>
private void MoveTileImmediate(int x, int y)
{
// 获取要移动的碎片
GameObject tile = tiles[x, y];
if (tile == null)
return;
// 计算一维索引
int fromIndex = y * gridSize + x;
int toIndex = emptyTilePosition.y * gridSize + emptyTilePosition.x;
// 计算目标位置
Vector3 targetPosition = CalculateWorldPosition(emptyTilePosition.x, emptyTilePosition.y);
// 立即移动
tile.transform.position = targetPosition;
// 更新数据结构
tiles[emptyTilePosition.x, emptyTilePosition.y] = tile;
tiles[x, y] = null;
// 交换状态
puzzleState[toIndex] = puzzleState[fromIndex];
puzzleState[fromIndex] = 0;
// 更新空白格位置
emptyTilePosition = new Vector2Int(x, y);
}
/// <summary>
/// 交换两个非空白格子以改变拼图的奇偶性
/// </summary>
private void SwapTwoNonEmptyTiles()
{
// 找到两个非空白格子
List<Vector2Int> nonEmptyPositions = new List<Vector2Int>();
for (int y = 0; y < gridSize; y++)
{
for (int x = 0; x < gridSize; x++)
{
if (tiles[x, y] != null)
{
nonEmptyPositions.Add(new Vector2Int(x, y));
}
}
}
// 如果有至少两个非空白格子
if (nonEmptyPositions.Count >= 2)
{
// 随机选择两个
int index1 = Random.Range(0, nonEmptyPositions.Count);
int index2 = (index1 + 1) % nonEmptyPositions.Count;
Vector2Int pos1 = nonEmptyPositions[index1];
Vector2Int pos2 = nonEmptyPositions[index2];
// 交换游戏对象
GameObject temp = tiles[pos1.x, pos1.y];
tiles[pos1.x, pos1.y] = tiles[pos2.x, pos2.y];
tiles[pos2.x, pos2.y] = temp;
// 交换位置
Vector3 tempPos = tiles[pos1.x, pos1.y].transform.position;
tiles[pos1.x, pos1.y].transform.position = tiles[pos2.x, pos2.y].transform.position;
tiles[pos2.x, pos2.y].transform.position = tempPos;
// 交换状态
int index1D1 = pos1.y * gridSize + pos1.x;
int index1D2 = pos2.y * gridSize + pos2.x;
int temp1D = puzzleState[index1D1];
puzzleState[index1D1] = puzzleState[index1D2];
puzzleState[index1D2] = temp1D;
}
}
/// <summary>
/// 清理拼图
/// </summary>
private void ClearPuzzle()
{
if (puzzleContainer != null)
{
// 销毁所有子物体
foreach (Transform child in puzzleContainer)
{
Destroy(child.gameObject);
}
}
// 重置数据
tiles = null;
puzzleState = null;
}
/// <summary>
/// 检查拼图是否已完成
/// </summary>
public bool IsPuzzleSolved()
{
for (int i = 0; i < puzzleState.Length - 1; i++)
{
if (puzzleState[i] != i + 1)
return false;
}
// 最后一个应该是空白格
return puzzleState[puzzleState.Length - 1] == 0;
}
}
// 碎片点击处理器
public class TileClickHandler : MonoBehaviour
{
private System.Action onClickCallback;
public void Initialize(System.Action callback)
{
onClickCallback = callback;
}
private void OnMouseDown()
{
onClickCallback?.Invoke();
}
}
这个碎片管理器提供了完整的拼图碎片生成、移动和打乱功能。它维护了游戏状态,并通过事件通知其他组件碎片移动情况。此外,它还确保拼图在打乱后是可解的,避免玩家遇到无法完成的拼图。
9.4.4 用户输入处理与交互优化
为了提供良好的用户体验,我们需要优化游戏的输入处理和交互方式:
csharp
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.EventSystems;
public class PuzzleInputManager : MonoBehaviour
{
// 引用
[SerializeField] private PuzzleTileManager tileManager;
[SerializeField] private Camera mainCamera;
// 触摸设置
[SerializeField] private float swipeThreshold = 20f;
[SerializeField] private float tapTimeThreshold = 0.2f;
// 触摸状态
private Vector2 touchStartPosition;
private float touchStartTime;
private bool isTouching = false;
// 键盘设置
[SerializeField] private KeyCode upKey = KeyCode.UpArrow;
[SerializeField] private KeyCode downKey = KeyCode.DownArrow;
[SerializeField] private KeyCode leftKey = KeyCode.LeftArrow;
[SerializeField] private KeyCode rightKey = KeyCode.RightArrow;
[SerializeField] private KeyCode restartKey = KeyCode.R;
[SerializeField] private KeyCode hintKey = KeyCode.H;
// 事件
public System.Action onRestartKeyPressed;
public System.Action onHintKeyPressed;
// 初始化
private void Start()
{
if (mainCamera == null)
mainCamera = Camera.main;
if (tileManager == null)
tileManager = FindObjectOfType<PuzzleTileManager>();
}
// 更新
private void Update()
{
// 处理键盘输入
HandleKeyboardInput();
// 处理触摸/鼠标输入
HandleTouchInput();
}
/// <summary>
/// 处理键盘输入
/// </summary>
private void HandleKeyboardInput()
{
// 方向键移动空白格
if (Input.GetKeyDown(upKey))
{
MoveTileInDirection(Vector2Int.down);
}
else if (Input.GetKeyDown(downKey))
{
MoveTileInDirection(Vector2Int.up);
}
else if (Input.GetKeyDown(leftKey))
{
MoveTileInDirection(Vector2Int.right);
}
else if (Input.GetKeyDown(rightKey))
{
MoveTileInDirection(Vector2Int.left);
}
// 重启键
if (Input.GetKeyDown(restartKey))
{
onRestartKeyPressed?.Invoke();
}
// 提示键
if (Input.GetKeyDown(hintKey))
{
onHintKeyPressed?.Invoke();
}
}
/// <summary>
/// 在指定方向移动与空白格相邻的碎片
/// </summary>
private void MoveTileInDirection(Vector2Int direction)
{
Vector2Int emptyPos = tileManager.EmptyPosition;
Vector2Int tilePos = emptyPos + direction;
// 检查边界
if (tilePos.x >= 0 && tilePos.x < tileManager.GridSize &&
tilePos.y >= 0 && tilePos.y < tileManager.GridSize)
{
tileManager.HandleTileClick(tilePos);
}
}
/// <summary>
/// 处理触摸/鼠标输入
/// </summary>
private void HandleTouchInput()
{
// 检测触摸/鼠标按下
if (Input.GetMouseButtonDown(0) && !IsPointerOverUI())
{
touchStartPosition = Input.mousePosition;
touchStartTime = Time.time;
isTouching = true;
}
// 检测触摸/鼠标抬起
if (Input.GetMouseButtonUp(0) && isTouching)
{
isTouching = false;
Vector2 touchEndPosition = Input.mousePosition;
float touchDuration = Time.time - touchStartTime;
// 计算移动距离和方向
Vector2 touchDelta = touchEndPosition - touchStartPosition;
float touchDistance = touchDelta.magnitude;
// 如果是点击(短时间内小幅度移动)
if (touchDistance < swipeThreshold && touchDuration < tapTimeThreshold)
{
HandleTap(touchEndPosition);
}
// 如果是滑动(大幅度移动)
else if (touchDistance >= swipeThreshold)
{
HandleSwipe(touchDelta);
}
}
}
/// <summary>
/// 处理点击事件
/// </summary>
private void HandleTap(Vector2 tapPosition)
{
// 转换屏幕坐标到世界坐标
Vector3 worldPosition = mainCamera.ScreenToWorldPoint(new Vector3(tapPosition.x, tapPosition.y, 10));
// 射线检测点击的物体
RaycastHit2D hit = Physics2D.Raycast(worldPosition, Vector2.zero);
if (hit.collider != null)
{
// 获取点击的碎片
TileClickHandler clickHandler = hit.collider.GetComponent<TileClickHandler>();
if (clickHandler != null)
{
// 触发点击处理
clickHandler.OnMouseDown();
}
}
}
/// <summary>
/// 处理滑动事件
/// </summary>
private void HandleSwipe(Vector2 swipeDelta)
{
// 确定主要方向
if (Mathf.Abs(swipeDelta.x) > Mathf.Abs(swipeDelta.y))
{
// 水平滑动
if (swipeDelta.x > 0)
{
MoveTileInDirection(Vector2Int.left); // 向右滑动,左边的碎片移到空白格
}
else
{
MoveTileInDirection(Vector2Int.right); // 向左滑动,右边的碎片移到空白格
}
}
else
{
// 垂直滑动
if (swipeDelta.y > 0)
{
MoveTileInDirection(Vector2Int.down); // 向上滑动,下边的碎片移到空白格
}
else
{
MoveTileInDirection(Vector2Int.up); // 向下滑动,上边的碎片移到空白格
}
}
}
/// <summary>
/// 检查指针是否在UI元素上
/// </summary>
private bool IsPointerOverUI()
{
return EventSystem.current != null && EventSystem.current.IsPointerOverGameObject();
}
}
这个输入管理器处理各种输入方式,包括键盘、鼠标点击和触摸屏滑动。它提供了多种控制拼图的方式,使游戏可以适应不同的平台和用户习惯。此外,它还检测UI交互,避免点击UI元素时意外触发游戏操作。
9.4.5 游戏进度保存与加载
为了保存玩家的游戏进度和成绩,我们需要实现存档系统:
csharp
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
public class PuzzleGameSaver : MonoBehaviour
{
// 存档文件路径
private string saveFilePath;
// 事件
public delegate void GameDataLoadedHandler(GameData data);
public event GameDataLoadedHandler OnGameDataLoaded;
// 游戏数据类
[System.Serializable]
public class GameData
{
// 玩家信息
public string playerName;
public int totalStars;
public int unlockedLevelCount;
// 关卡信息
public List<LevelData> levelDataList;
// 设置
public float musicVolume;
public float sfxVolume;
public bool vibrationEnabled;
public GameData()
{
playerName = "Player";
totalStars = 0;
unlockedLevelCount = 1;
levelDataList = new List<LevelData>();
musicVolume = 1.0f;
sfxVolume = 1.0f;
vibrationEnabled = true;
}
}
// 关卡数据类
[System.Serializable]
public class LevelData
{
public int levelIndex;
public bool isCompleted;
public int bestMoves;
public float bestTime;
public int starCount;
public LevelData(int index)
{
levelIndex = index;
isCompleted = false;
bestMoves = int.MaxValue;
bestTime = float.MaxValue;
starCount = 0;
}
}
// 当前游戏数据
private GameData gameData;
// 初始化
private void Awake()
{
// 设置保存路径
saveFilePath = Path.Combine(Application.persistentDataPath, "puzzlegame.sav");
// 加载游戏数据
LoadGameData();
}
/// <summary>
/// 加载游戏数据
/// </summary>
private void LoadGameData()
{
// 检查文件是否存在
if (File.Exists(saveFilePath))
{
try
{
// 打开文件
using (FileStream file = File.Open(saveFilePath, FileMode.Open))
{
// 创建二进制格式化器
BinaryFormatter formatter = new BinaryFormatter();
// 反序列化
gameData = (GameData)formatter.Deserialize(file);
Debug.Log("游戏数据加载成功");
}
// 触发加载事件
OnGameDataLoaded?.Invoke(gameData);
}
catch (System.Exception e)
{
Debug.LogError("加载游戏数据失败: " + e.Message);
CreateNewGameData();
}
}
else
{
CreateNewGameData();
}
}
/// <summary>
/// 创建新的游戏数据
/// </summary>
private void CreateNewGameData()
{
gameData = new GameData();
Debug.Log("创建新的游戏数据");
// 触发加载事件
OnGameDataLoaded?.Invoke(gameData);
}
/// <summary>
/// 保存游戏数据
/// </summary>
public void SaveGameData()
{
try
{
// 创建文件
using (FileStream file = File.Create(saveFilePath))
{
// 创建二进制格式化器
BinaryFormatter formatter = new BinaryFormatter();
// 序列化
formatter.Serialize(file, gameData);
}
Debug.Log("游戏数据保存成功");
}
catch (System.Exception e)
{
Debug.LogError("保存游戏数据失败: " + e.Message);
}
}
/// <summary>
/// 更新关卡成绩
/// </summary>
/// <param name="levelIndex">关卡索引</param>
/// <param name="isCompleted">是否完成</param>
/// <param name="moves">移动次数</param>
/// <param name="time">用时(秒)</param>
/// <param name="stars">星级评价</param>
public void UpdateLevelStats(int levelIndex, bool isCompleted, int moves, float time, int stars)
{
// 查找关卡数据
LevelData levelData = gameData.levelDataList.Find(data => data.levelIndex == levelIndex);
// 如果不存在,创建新的
if (levelData == null)
{
levelData = new LevelData(levelIndex);
gameData.levelDataList.Add(levelData);
}
// 更新完成状态
if (isCompleted)
{
levelData.isCompleted = true;
// 解锁下一关
if (levelIndex >= gameData.unlockedLevelCount - 1)
{
gameData.unlockedLevelCount = levelIndex + 2; // +2因为索引从0开始
}
}
// 更新最佳成绩
if (moves < levelData.bestMoves || levelData.bestMoves == int.MaxValue)
{
levelData.bestMoves = moves;
}
if (time < levelData.bestTime || levelData.bestTime == float.MaxValue)
{
levelData.bestTime = time;
}
// 更新星级
if (stars > levelData.starCount)
{
// 增加总星数
gameData.totalStars += (stars - levelData.starCount);
levelData.starCount = stars;
}
// 保存更新后的数据
SaveGameData();
}
/// <summary>
/// 获取关卡数据
/// </summary>
/// <param name="levelIndex">关卡索引</param>
/// <returns>关卡数据</returns>
public LevelData GetLevelData(int levelIndex)
{
LevelData levelData = gameData.levelDataList.Find(data => data.levelIndex == levelIndex);
// 如果不存在,创建一个新的
if (levelData == null)
{
levelData = new LevelData(levelIndex);
gameData.levelDataList.Add(levelData);
SaveGameData();
}
return levelData;
}
/// <summary>
/// 获取已解锁的关卡数量
/// </summary>
public int GetUnlockedLevelCount()
{
return gameData.unlockedLevelCount;
}
/// <summary>
/// 获取总星数
/// </summary>
public int GetTotalStars()
{
return gameData.totalStars;
}
/// <summary>
/// 设置玩家姓名
/// </summary>
public void SetPlayerName(string name)
{
gameData.playerName = name;
SaveGameData();
}
/// <summary>
/// 获取玩家姓名
/// </summary>
public string GetPlayerName()
{
return gameData.playerName;
}
/// <summary>
/// 设置音乐音量
/// </summary>
public void SetMusicVolume(float volume)
{
gameData.musicVolume = volume;
SaveGameData();
}
/// <summary>
/// 获取音乐音量
/// </summary>
public float GetMusicVolume()
{
return gameData.musicVolume;
}
/// <summary>
/// 设置音效音量
/// </summary>
public void SetSfxVolume(float volume)
{
gameData.sfxVolume = volume;
SaveGameData();
}
/// <summary>
/// 获取音效音量
/// </summary>
public float GetSfxVolume()
{
return gameData.sfxVolume;
}
/// <summary>
/// 设置振动启用状态
/// </summary>
public void SetVibrationEnabled(bool enabled)
{
gameData.vibrationEnabled = enabled;
SaveGameData();
}
/// <summary>
/// 获取振动启用状态
/// </summary>
public bool GetVibrationEnabled()
{
return gameData.vibrationEnabled;
}
/// <summary>
/// 清除所有游戏数据
/// </summary>
public void ClearAllData()
{
if (File.Exists(saveFilePath))
{
File.Delete(saveFilePath);
}
CreateNewGameData();
}
}
这个游戏存档系统使用二进制序列化来保存和加载游戏数据,包括玩家信息、关卡进度和游戏设置。它提供了一系列方法来更新和检索各种游戏数据,确保玩家的进度可以在游戏会话之间保持。
9.5 拼图游戏的进阶功能与优化
9.5.1 性能优化与内存管理
在移动设备上运行拼图游戏时,性能优化和内存管理尤为重要:
csharp
using UnityEngine;
using System.Collections.Generic;
public class PuzzleGameOptimizer : MonoBehaviour
{
// 内存管理
[SerializeField] private bool useObjectPooling = true;
[SerializeField] private int initialPoolSize = 20;
// 对象池
private Dictionary<string, Queue<GameObject>> objectPools = new Dictionary<string, Queue<GameObject>>();
// 纹理缓存
private Dictionary<int, List<Texture2D>> textureCache = new Dictionary<int, List<Texture2D>>();
// 监控设置
[SerializeField] private bool showPerformanceStats = false;
private float fpsUpdateInterval = 0.5f;
private float fpsAccumulator = 0;
private int frameCount = 0;
private float currentFps = 0;
private long currentMemoryUsage = 0;
private GUIStyle statsStyle;
// 初始化
private void Awake()
{
if (useObjectPooling)
{
InitializeObjectPools();
}
// 设置应用程序目标帧率
Application.targetFrameRate = 60;
}
private void Start()
{
// 初始化GUI样式
statsStyle = new GUIStyle();
statsStyle.fontSize = 20;
statsStyle.normal.textColor = Color.white;
}
private void Update()
{
if (showPerformanceStats)
{
// 更新帧率
fpsAccumulator += Time.unscaledDeltaTime;
frameCount++;
if (fpsAccumulator >= fpsUpdateInterval)
{
currentFps = frameCount / fpsAccumulator;
frameCount = 0;
fpsAccumulator = 0;
// 更新内存使用
currentMemoryUsage = System.GC.GetTotalMemory(false) / (1024 * 1024); // MB
}
}
}
private void OnGUI()
{
if (showPerformanceStats)
{
GUI.Label(new Rect(10, 10, 200, 40), $"FPS: {currentFps:F1}", statsStyle);
GUI.Label(new Rect(10, 40, 200, 40), $"内存: {currentMemoryUsage}MB", statsStyle);
}
}
/// <summary>
/// 初始化对象池
/// </summary>
private void InitializeObjectPools()
{
// 预先创建常用对象池
CreatePool("TilePrefab", Resources.Load<GameObject>("Prefabs/TilePrefab"), initialPoolSize);
}
/// <summary>
/// 创建对象池
/// </summary>
public void CreatePool(string poolName, GameObject prefab, int size)
{
if (prefab == null)
{
Debug.LogError($"无法创建对象池: 预制件 {poolName} 不存在");
return;
}
// 创建新池
Queue<GameObject> pool = new Queue<GameObject>();
// 预先创建对象
for (int i = 0; i < size; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
objectPools[poolName] = pool;
Debug.Log($"创建对象池: {poolName}, 大小: {size}");
}
/// <summary>
/// 从池中获取对象
/// </summary>
public GameObject GetObjectFromPool(string poolName)
{
if (!objectPools.ContainsKey(poolName))
{
Debug.LogWarning($"对象池 {poolName} 不存在");
return null;
}
Queue<GameObject> pool = objectPools[poolName];
// 如果池为空,扩展它
if (pool.Count == 0)
{
Debug.Log($"扩展对象池: {poolName}");
GameObject prefab = Resources.Load<GameObject>($"Prefabs/{poolName}");
if (prefab != null)
{
for (int i = 0; i < initialPoolSize / 2; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
else
{
Debug.LogError($"无法扩展对象池: 预制件 {poolName} 不存在");
return null;
}
}
// 获取对象
GameObject pooledObject = pool.Dequeue();
pooledObject.SetActive(true);
return pooledObject;
}
/// <summary>
/// 将对象返回到池中
/// </summary>
public void ReturnObjectToPool(string poolName, GameObject obj)
{
if (!objectPools.ContainsKey(poolName))
{
objectPools[poolName] = new Queue<GameObject>();
}
// 重置对象状态
obj.SetActive(false);
// 返回到池
objectPools[poolName].Enqueue(obj);
}
/// <summary>
/// 缓存切分后的纹理
/// </summary>
public void CacheTextures(int gridSize, List<Texture2D> textures)
{
if (!textureCache.ContainsKey(gridSize))
{
textureCache[gridSize] = new List<Texture2D>();
}
textureCache[gridSize] = textures;
}
/// <summary>
/// 获取缓存的纹理
/// </summary>
public List<Texture2D> GetCachedTextures(int gridSize)
{
if (textureCache.ContainsKey(gridSize))
{
return textureCache[gridSize];
}
return null;
}
/// <summary>
/// 清理缓存的纹理
/// </summary>
public void ClearTextureCache(int gridSize = -1)
{
if (gridSize < 0)
{
// 清理所有缓存
foreach (var entry in textureCache)
{
foreach (var texture in entry.Value)
{
Destroy(texture);
}
}
textureCache.Clear();
}
else if (textureCache.ContainsKey(gridSize))
{
// 清理特定大小的缓存
foreach (var texture in textureCache[gridSize])
{
Destroy(texture);
}
textureCache.Remove(gridSize);
}
// 强制垃圾回收
System.GC.Collect();
}
/// <summary>
/// 优化给定的纹理以减少内存使用
/// </summary>
public Texture2D OptimizeTexture(Texture2D texture, int maxSize = 1024)
{
// 如果纹理太大,调整大小
if (texture.width > maxSize || texture.height > maxSize)
{
return ResizeTexture(texture, maxSize);
}
return texture;
}
/// <summary>
/// 调整纹理大小
/// </summary>
private Texture2D ResizeTexture(Texture2D source, int maxSize)
{
// 计算新尺寸
int width = source.width;
int height = source.height;
float aspect = (float)width / height;
if (width > height)
{
width = maxSize;
height = Mathf.RoundToInt(width / aspect);
}
else
{
height = maxSize;
width = Mathf.RoundToInt(height * aspect);
}
// 创建临时RenderTexture
RenderTexture rt = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32);
// 将源纹理渲染到RenderTexture
Graphics.Blit(source, rt);
// 保存当前的活动RenderTexture
RenderTexture prev = RenderTexture.active;
RenderTexture.active = rt;
// 创建新的纹理
Texture2D resized = new Texture2D(width, height);
resized.ReadPixels(new Rect(0, 0, width, height), 0, 0);
resized.Apply();
// 恢复先前的RenderTexture
RenderTexture.active = prev;
RenderTexture.ReleaseTemporary(rt);
return resized;
}
/// <summary>
/// 应用质量设置
/// </summary>
public void ApplyQualitySettings(int qualityLevel)
{
// 设置Unity质量级别
QualitySettings.SetQualityLevel(qualityLevel, true);
switch (qualityLevel)
{
case 0: // 低
QualitySettings.vSyncCount = 0;
QualitySettings.antiAliasing = 0;
QualitySettings.shadows = ShadowQuality.Disable;
break;
case 1: // 中
QualitySettings.vSyncCount = 1;
QualitySettings.antiAliasing = 0;
QualitySettings.shadows = ShadowQuality.HardOnly;
break;
case 2: // 高
QualitySettings.vSyncCount = 1;
QualitySettings.antiAliasing = 2;
QualitySettings.shadows = ShadowQuality.All;
break;
}
}
/// <summary>
/// 自动检测并应用最佳质量设置
/// </summary>
public void AutoDetectQuality()
{
// 基于设备性能自动选择质量级别
int qualityLevel;
// 检查系统内存
int systemMemoryMB = SystemInfo.systemMemorySize;
// 检查GPU内存
int gpuMemoryMB = SystemInfo.graphicsMemorySize;
// 检查CPU核心数
int cpuCores = SystemInfo.processorCount;
if (systemMemoryMB < 2048 || gpuMemoryMB < 512 || cpuCores < 4)
{
qualityLevel = 0; // 低质量
}
else if (systemMemoryMB < 4096 || gpuMemoryMB < 1024 || cpuCores < 6)
{
qualityLevel = 1; // 中质量
}
else
{
qualityLevel = 2; // 高质量
}
ApplyQualitySettings(qualityLevel);
Debug.Log($"自动检测质量级别: {qualityLevel}");
}
}
这个优化器类提供了对象池、纹理缓存和性能监控功能。对象池可以减少频繁创建和销毁游戏对象的开销,而纹理缓存则可以避免重复生成纹理,从而降低内存占用和提高性能。此外,它还提供了自动质量设置功能,可以根据设备性能调整游戏的图形质量。
9.5.2 特效系统与视觉反馈
为了提升游戏的视觉体验,我们可以添加各种特效和视觉反馈:
csharp
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class PuzzleVisualEffects : MonoBehaviour
{
// 特效预制件
[SerializeField] private GameObject tileMoveEffectPrefab;
[SerializeField] private GameObject tileCorrectEffectPrefab;
[SerializeField] private GameObject victoryEffectPrefab;
// 着色器效果
[SerializeField] private Material outlineMaterial;
[SerializeField] private Material glowMaterial;
// 相机震动设置
[SerializeField] private float shakeDuration = 0.2f;
[SerializeField] private float shakeMagnitude = 0.05f;
// 引用
private Camera mainCamera;
private Transform effectContainer;
// 初始化
private void Awake()
{
mainCamera = Camera.main;
// 创建特效容器
effectContainer = new GameObject("EffectContainer").transform;
effectContainer.SetParent(transform);
}
/// <summary>
/// 播放移动特效
/// </summary>
public void PlayMoveEffect(Vector3 position)
{
if (tileMoveEffectPrefab != null)
{
GameObject effect = Instantiate(tileMoveEffectPrefab, position, Quaternion.identity, effectContainer);
// 2秒后销毁特效
Destroy(effect, 2f);
}
}
/// <summary>
/// 播放正确放置特效
/// </summary>
public void PlayCorrectPlacementEffect(Vector3 position)
{
if (tileCorrectEffectPrefab != null)
{
GameObject effect = Instantiate(tileCorrectEffectPrefab, position, Quaternion.identity, effectContainer);
// 2秒后销毁特效
Destroy(effect, 2f);
}
}
/// <summary>
/// 播放胜利特效
/// </summary>
public void PlayVictoryEffect()
{
if (victoryEffectPrefab != null)
{
// 在屏幕中心创建特效
GameObject effect = Instantiate(victoryEffectPrefab, Vector3.zero, Quaternion.identity, effectContainer);
// 5秒后销毁特效
Destroy(effect, 5f);
}
// 相机震动
StartCoroutine(ShakeCamera());
}
/// <summary>
/// 相机震动协程
/// </summary>
private IEnumerator ShakeCamera()
{
Vector3 originalPosition = mainCamera.transform.position;
float elapsed = 0f;
while (elapsed < shakeDuration)
{
float x = Random.Range(-1f, 1f) * shakeMagnitude;
float y = Random.Range(-1f, 1f) * shakeMagnitude;
mainCamera.transform.position = new Vector3(originalPosition.x + x, originalPosition.y + y, originalPosition.z);
elapsed += Time.deltaTime;
yield return null;
}
mainCamera.transform.position = originalPosition;
}
/// <summary>
/// 添加轮廓效果
/// </summary>
public void AddOutlineEffect(SpriteRenderer renderer)
{
if (outlineMaterial != null && renderer != null)
{
// 保存原始材质
Material originalMaterial = renderer.material;
// 创建材质实例
Material outlineInstance = new Material(outlineMaterial);
// 设置原始纹理
outlineInstance.mainTexture = originalMaterial.mainTexture;
// 应用新材质
renderer.material = outlineInstance;
}
}
/// <summary>
/// 添加发光效果
/// </summary>
public void AddGlowEffect(SpriteRenderer renderer)
{
if (glowMaterial != null && renderer != null)
{
// 保存原始材质
Material originalMaterial = renderer.material;
// 创建材质实例
Material glowInstance = new Material(glowMaterial);
// 设置原始纹理
glowInstance.mainTexture = originalMaterial.mainTexture;
// 应用新材质
renderer.material = glowInstance;
}
}
/// <summary>
/// 移除特殊效果
/// </summary>
public void RemoveEffect(SpriteRenderer renderer, Material originalMaterial)
{
if (renderer != null)
{
renderer.material = originalMaterial;
}
}
/// <summary>
/// 淡入淡出特效
/// </summary>
public IEnumerator FadeEffect(SpriteRenderer renderer, float duration, bool fadeIn)
{
if (renderer == null)
yield break;
Color startColor = renderer.color;
Color targetColor = startColor;
if (fadeIn)
{
startColor.a = 0;
renderer.color = startColor;
}
else
{
targetColor.a = 0;
}
float elapsed = 0f;
while (elapsed < duration)
{
float t = elapsed / duration;
renderer.color = Color.Lerp(startColor, targetColor, t);
elapsed += Time.deltaTime;
yield return null;
}
renderer.color = targetColor;
}
/// <summary>
/// 缩放特效
/// </summary>
public IEnumerator ScaleEffect(Transform target, Vector3 startScale, Vector3 endScale, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
float t = elapsed / duration;
t = Mathf.SmoothStep(0, 1, t); // 使用平滑步进函数
target.localScale = Vector3.Lerp(startScale, endScale, t);
elapsed += Time.deltaTime;
yield return null;
}
target.localScale = endScale;
}
/// <summary>
/// 创建闪光效果
/// </summary>
public void CreateFlashEffect(Vector3 position, Color color, float size = 1f)
{
// 创建精灵对象
GameObject flashObj = new GameObject("FlashEffect");
flashObj.transform.position = position;
flashObj.transform.SetParent(effectContainer);
// 添加精灵渲染器
SpriteRenderer renderer = flashObj.AddComponent<SpriteRenderer>();
renderer.sprite = CreateCircleSprite(color);
renderer.sortingOrder = 10; // 确保在前景
// 设置大小
flashObj.transform.localScale = Vector3.one * size;
// 添加闪光行为
StartCoroutine(FlashBehavior(flashObj));
}
/// <summary>
/// 闪光行为
/// </summary>
private IEnumerator FlashBehavior(GameObject flashObj)
{
SpriteRenderer renderer = flashObj.GetComponent<SpriteRenderer>();
// 淡入
yield return StartCoroutine(FadeEffect(renderer, 0.1f, true));
// 扩大
yield return StartCoroutine(ScaleEffect(flashObj.transform, flashObj.transform.localScale, flashObj.transform.localScale * 2f, 0.2f));
// 淡出
yield return StartCoroutine(FadeEffect(renderer, 0.3f, false));
// 销毁对象
Destroy(flashObj);
}
/// <summary>
/// 创建圆形精灵
/// </summary>
private Sprite CreateCircleSprite(Color color)
{
int resolution = 64;
Texture2D texture = new Texture2D(resolution, resolution);
Vector2 center = new Vector2(resolution / 2, resolution / 2);
float radius = resolution / 2;
for (int y = 0; y < resolution; y++)
{
for (int x = 0; x < resolution; x++)
{
float distance = Vector2.Distance(new Vector2(x, y), center);
if (distance < radius)
{
float alpha = 1 - (distance / radius);
texture.SetPixel(x, y, new Color(color.r, color.g, color.b, alpha));
}
else
{
texture.SetPixel(x, y, Color.clear);
}
}
}
texture.Apply();
return Sprite.Create(texture, new Rect(0, 0, resolution, resolution), new Vector2(0.5f, 0.5f));
}
}
这个特效系统提供了多种视觉效果,如移动特效、正确放置特效和胜利特效。它使用粒子系统、材质变换和相机震动来增强游戏的视觉体验。此外,它还包含了淡入淡出、缩放和闪光等动画效果,可以为游戏添加更多的视觉吸引力。
9.5.3 多语言支持与本地化
为了使游戏适应国际市场,我们需要实现多语言支持:
csharp
using UnityEngine;
using System.Collections.Generic;
using TMPro;
using UnityEngine.UI;
using System.IO;
public class LocalizationManager : MonoBehaviour
{
// 单例实例
private static LocalizationManager _instance;
public static LocalizationManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<LocalizationManager>();
if (_instance == null)
{
GameObject obj = new GameObject("LocalizationManager");
_instance = obj.AddComponent<LocalizationManager>();
DontDestroyOnLoad(obj);
}
}
return _instance;
}
}
// 支持的语言
public enum Language
{
English,
Chinese,
Japanese,
Korean,
French,
German,
Spanish,
Russian
}
// 当前语言
private Language currentLanguage = Language.English;
// 本地化数据
private Dictionary<string, Dictionary<Language, string>> localizationData = new Dictionary<string, Dictionary<Language, string>>();
// 字体映射
private Dictionary<Language, TMP_FontAsset> languageFonts = new Dictionary<Language, TMP_FontAsset>();
// 事件
public delegate void LanguageChangedHandler(Language newLanguage);
public event LanguageChangedHandler OnLanguageChanged;
// 初始化
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
// 加载本地化数据
LoadLocalizationData();
// 加载字体
LoadFonts();
// 设置初始语言
SetLanguage(GetSystemLanguage());
}
/// <summary>
/// 加载本地化数据
/// </summary>
private void LoadLocalizationData()
{
TextAsset localizationFile = Resources.Load<TextAsset>("Localization/localization");
if (localizationFile == null)
{
Debug.LogError("本地化文件未找到!");
return;
}
string[] lines = localizationFile.text.Split('\n');
// 读取标题行
string[] headers = lines[0].Trim().Split('\t');
// 从第二行开始读取数据
for (int i = 1; i < lines.Length; i++)
{
string line = lines[i].Trim();
if (string.IsNullOrEmpty(line))
continue;
string[] columns = line.Split('\t');
if (columns.Length < 2)
continue;
string key = columns[0];
Dictionary<Language, string> translations = new Dictionary<Language, string>();
for (int j = 1; j < columns.Length && j < headers.Length; j++)
{
Language language = (Language)System.Enum.Parse(typeof(Language), headers[j]);
translations[language] = columns[j];
}
localizationData[key] = translations;
}
Debug.Log($"已加载 {localizationData.Count} 条本地化条目");
}
/// <summary>
/// 加载字体
/// </summary>
private void LoadFonts()
{
// 加载不同语言的字体资源
languageFonts[Language.English] = Resources.Load<TMP_FontAsset>("Fonts/Arial");
languageFonts[Language.Chinese] = Resources.Load<TMP_FontAsset>("Fonts/NotoSansSC");
languageFonts[Language.Japanese] = Resources.Load<TMP_FontAsset>("Fonts/NotoSansJP");
languageFonts[Language.Korean] = Resources.Load<TMP_FontAsset>("Fonts/NotoSansKR");
languageFonts[Language.French] = Resources.Load<TMP_FontAsset>("Fonts/Arial");
languageFonts[Language.German] = Resources.Load<TMP_FontAsset>("Fonts/Arial");
languageFonts[Language.Spanish] = Resources.Load<TMP_FontAsset>("Fonts/Arial");
languageFonts[Language.Russian] = Resources.Load<TMP_FontAsset>("Fonts/Arial");
}
/// <summary>
/// 根据系统设置获取初始语言
/// </summary>
private Language GetSystemLanguage()
{
// 尝试从玩家偏好设置加载
int savedLanguage = PlayerPrefs.GetInt("Language", -1);
if (savedLanguage >= 0 && savedLanguage < System.Enum.GetValues(typeof(Language)).Length)
{
return (Language)savedLanguage;
}
// 否则根据系统语言设置
switch (Application.systemLanguage)
{
case SystemLanguage.English:
return Language.English;
case SystemLanguage.Chinese:
case SystemLanguage.ChineseSimplified:
case SystemLanguage.ChineseTraditional:
return Language.Chinese;
case SystemLanguage.Japanese:
return Language.Japanese;
case SystemLanguage.Korean:
return Language.Korean;
case SystemLanguage.French:
return Language.French;
case SystemLanguage.German:
return Language.German;
case SystemLanguage.Spanish:
return Language.Spanish;
case SystemLanguage.Russian:
return Language.Russian;
default:
return Language.English;
}
}
/// <summary>
/// 设置当前语言
/// </summary>
public void SetLanguage(Language language)
{
currentLanguage = language;
// 保存选择
PlayerPrefs.SetInt("Language", (int)language);
PlayerPrefs.Save();
// 触发事件
OnLanguageChanged?.Invoke(language);
// 更新当前场景中的所有本地化文本
LocalizedText[] texts = FindObjectsOfType<LocalizedText>();
foreach (LocalizedText text in texts)
{
text.UpdateText();
}
}
/// <summary>
/// 获取本地化文本
/// </summary>
public string GetLocalizedText(string key)
{
if (localizationData.TryGetValue(key, out Dictionary<Language, string> translations))
{
if (translations.TryGetValue(currentLanguage, out string translation))
{
return translation;
}
// 如果没有当前语言的翻译,回退到英语
if (translations.TryGetValue(Language.English, out string englishTranslation))
{
return englishTranslation;
}
}
// 如果没有找到任何翻译,返回键名
return key;
}
/// <summary>
/// 获取当前语言的字体
/// </summary>
public TMP_FontAsset GetFontForCurrentLanguage()
{
if (languageFonts.TryGetValue(currentLanguage, out TMP_FontAsset font))
{
return font;
}
// 如果没有找到,返回英语字体
return languageFonts[Language.English];
}
/// <summary>
/// 获取当前语言
/// </summary>
public Language GetCurrentLanguage()
{
return currentLanguage;
}
/// <summary>
/// 获取语言名称
/// </summary>
public string GetLanguageName(Language language)
{
switch (language)
{
case Language.English: return "English";
case Language.Chinese: return "中文";
case Language.Japanese: return "日本語";
case Language.Korean: return "한국어";
case Language.French: return "Français";
case Language.German: return "Deutsch";
case Language.Spanish: return "Español";
case Language.Russian: return "Русский";
default: return "Unknown";
}
}
}
// 本地化文本组件
[RequireComponent(typeof(TextMeshProUGUI))]
public class LocalizedText : MonoBehaviour
{
// 本地化键
[SerializeField] private string localizationKey;
// 是否使用语言特定字体
[SerializeField] private bool useLanguageFont = true;
// 组件引用
private TextMeshProUGUI textComponent;
// 初始化
private void Awake()
{
textComponent = GetComponent<TextMeshProUGUI>();
// 如果没有设置键但有初始文本,使用初始文本作为键
if (string.IsNullOrEmpty(localizationKey) && textComponent != null)
{
localizationKey = textComponent.text;
}
}
private void Start()
{
// 注册语言变更事件
LocalizationManager.Instance.OnLanguageChanged += OnLanguageChanged;
// 初始更新
UpdateText();
}
private void OnDestroy()
{
// 取消注册事件
if (LocalizationManager.Instance != null)
{
LocalizationManager.Instance.OnLanguageChanged -= OnLanguageChanged;
}
}
// 语言变更处理
private void OnLanguageChanged(LocalizationManager.Language newLanguage)
{
UpdateText();
}
// 更新文本
public void UpdateText()
{
if (textComponent == null || string.IsNullOrEmpty(localizationKey))
return;
textComponent.text = LocalizationManager.Instance.GetLocalizedText(localizationKey);
if (useLanguageFont)
{
textComponent.font = LocalizationManager.Instance.GetFontForCurrentLanguage();
}
}
// 设置本地化键
public void SetKey(string key)
{
localizationKey = key;
UpdateText();
}
}
// 本地化图像组件
[RequireComponent(typeof(Image))]
public class LocalizedImage : MonoBehaviour
{
// 本地化键(用于在不同语言加载不同图像)
[SerializeField] private string localizationKey;
// 组件引用
private Image imageComponent;
// 初始化
private void Awake()
{
imageComponent = GetComponent<Image>();
}
private void Start()
{
// 注册语言变更事件
LocalizationManager.Instance.OnLanguageChanged += OnLanguageChanged;
// 初始更新
UpdateImage();
}
private void OnDestroy()
{
// 取消注册事件
if (LocalizationManager.Instance != null)
{
LocalizationManager.Instance.OnLanguageChanged -= OnLanguageChanged;
}
}
// 语言变更处理
private void OnLanguageChanged(LocalizationManager.Language newLanguage)
{
UpdateImage();
}
// 更新图像
private void UpdateImage()
{
if (imageComponent == null || string.IsNullOrEmpty(localizationKey))
return;
LocalizationManager.Language currentLanguage = LocalizationManager.Instance.GetCurrentLanguage();
// 加载特定语言的图像
string imagePath = $"Images/Localized/{localizationKey}_{currentLanguage}";
Sprite localizedSprite = Resources.Load<Sprite>(imagePath);
// 如果找不到特定语言的图像,加载默认图像
if (localizedSprite == null)
{
imagePath = $"Images/Localized/{localizationKey}_Default";
localizedSprite = Resources.Load<Sprite>(imagePath);
}
if (localizedSprite != null)
{
imageComponent.sprite = localizedSprite;
}
}
}
这个本地化系统允许游戏支持多种语言,包括英语、中文、日语等。它从本地化文件中加载翻译数据,并提供了自动更新文本和图像的组件。此外,它还支持根据语言设置不同的字体,确保文本在各种语言中都能正确显示。
9.6 拼图游戏的扩展与商业化
9.6.1 自定义拼图与照片导入
为了增加游戏的个性化程度,我们可以允许玩家导入自己的图片作为拼图:
csharp
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.IO;
using UnityEngine.Networking;
public class CustomPuzzleManager : MonoBehaviour
{
// UI引用
[SerializeField] private Button importButton;
[SerializeField] private Button cameraButton;
[SerializeField] private Image previewImage;
[SerializeField] private GameObject loadingIndicator;
// 事件
public delegate void ImageImportedHandler(Texture2D importedTexture);
public event ImageImportedHandler OnImageImported;
// 相机捕获设置
[SerializeField] private int captureWidth = 1280;
[SerializeField] private int captureHeight = 720;
// 初始化
private void Start()
{
if (importButton != null)
importButton.onClick.AddListener(ImportImage);
if (cameraButton != null)
cameraButton.onClick.AddListener(CaptureFromCamera);
if (loadingIndicator != null)
loadingIndicator.SetActive(false);
}
/// <summary>
/// 导入图像
/// </summary>
public void ImportImage()
{
#if UNITY_EDITOR
// 在编辑器中使用文件对话框
string path = UnityEditor.EditorUtility.OpenFilePanel("选择图像", "", "png,jpg,jpeg");
if (!string.IsNullOrEmpty(path))
{
StartCoroutine(LoadImageFromPath(path));
}
#elif UNITY_ANDROID || UNITY_IOS
// 移动平台使用原生插件
NativeGallery.Permission permission = NativeGallery.GetImageFromGallery((path) =>
{
if (!string.IsNullOrEmpty(path))
{
StartCoroutine(LoadImageFromPath(path));
}
}, "选择一张图片作为拼图", "image/*");
#endif
}
/// <summary>
/// 从相机捕获图像
/// </summary>
public void CaptureFromCamera()
{
#if UNITY_EDITOR
Debug.Log("相机捕获在编辑器中不可用");
#elif UNITY_ANDROID || UNITY_IOS
// 使用原生相机
NativeCamera.Permission permission = NativeCamera.TakePicture((path) =>
{
if (!string.IsNullOrEmpty(path))
{
StartCoroutine(LoadImageFromPath(path));
}
}, captureWidth, captureHeight);
#endif
}
/// <summary>
/// 从路径加载图像
/// </summary>
private IEnumerator LoadImageFromPath(string path)
{
if (loadingIndicator != null)
loadingIndicator.SetActive(true);
// 使用UnityWebRequest加载图像(支持各种平台)
using (UnityWebRequest request = UnityWebRequestTexture.GetTexture("file://" + path))
{
yield return request.SendWebRequest();
if (request.isNetworkError || request.isHttpError)
{
Debug.LogError("图像加载失败: " + request.error);
}
else
{
Texture2D texture = ((DownloadHandlerTexture)request.downloadHandler).texture;
// 确保纹理可读写
Texture2D readableTexture = MakeTextureReadable(texture);
// 处理图像(裁剪为正方形、调整大小等)
Texture2D processedTexture = ProcessImage(readableTexture);
// 更新预览
if (previewImage != null)
{
previewImage.sprite = Sprite.Create(
processedTexture,
new Rect(0, 0, processedTexture.width, processedTexture.height),
new Vector2(0.5f, 0.5f)
);
}
// 触发事件
OnImageImported?.Invoke(processedTexture);
// 保存到自定义拼图库
SaveToCustomPuzzleLibrary(processedTexture);
}
if (loadingIndicator != null)
loadingIndicator.SetActive(false);
}
}
/// <summary>
/// 确保纹理可读写
/// </summary>
private Texture2D MakeTextureReadable(Texture2D source)
{
// 创建临时RenderTexture
RenderTexture renderTexture = RenderTexture.GetTemporary(
source.width,
source.height,
0,
RenderTextureFormat.Default,
RenderTextureReadWrite.Linear
);
// 将源纹理复制到RenderTexture
Graphics.Blit(source, renderTexture);
// 保存当前活动的RenderTexture
RenderTexture previous = RenderTexture.active;
RenderTexture.active = renderTexture;
// 创建新的可读写纹理
Texture2D readableTexture = new Texture2D(source.width, source.height);
readableTexture.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
readableTexture.Apply();
// 恢复之前的RenderTexture
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(renderTexture);
return readableTexture;
}
/// <summary>
/// 处理图像(裁剪为正方形、调整大小等)
/// </summary>
private Texture2D ProcessImage(Texture2D source)
{
int size = Mathf.Min(source.width, source.height);
// 裁剪为正方形
int startX = (source.width - size) / 2;
int startY = (source.height - size) / 2;
Color[] pixels = source.GetPixels(startX, startY, size, size);
// 创建新纹理
Texture2D result = new Texture2D(size, size);
result.SetPixels(pixels);
result.Apply();
// 调整大小(如果需要)
if (size > 1024)
{
return ResizeTexture(result, 1024, 1024);
}
return result;
}
/// <summary>
/// 调整纹理大小
/// </summary>
private Texture2D ResizeTexture(Texture2D source, int targetWidth, int targetHeight)
{
// 创建临时RenderTexture
RenderTexture rt = RenderTexture.GetTemporary(targetWidth, targetHeight, 0, RenderTextureFormat.ARGB32);
// 将源纹理复制到RenderTexture(会自动调整大小)
Graphics.Blit(source, rt);
// 保存当前活动的RenderTexture
RenderTexture previous = RenderTexture.active;
RenderTexture.active = rt;
// 创建新的纹理
Texture2D result = new Texture2D(targetWidth, targetHeight);
result.ReadPixels(new Rect(0, 0, targetWidth, targetHeight), 0, 0);
result.Apply();
// 恢复之前的RenderTexture
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(rt);
return result;
}
/// <summary>
/// 保存到自定义拼图库
/// </summary>
private void SaveToCustomPuzzleLibrary(Texture2D texture)
{
string dirPath = Path.Combine(Application.persistentDataPath, "CustomPuzzles");
// 创建目录(如果不存在)
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
// 生成唯一文件名
string filename = "puzzle_" + System.DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".png";
string fullPath = Path.Combine(dirPath, filename);
// 将纹理保存为PNG
byte[] bytes = texture.EncodeToPNG();
File.WriteAllBytes(fullPath, bytes);
Debug.Log("保存自定义拼图: " + fullPath);
// 刷新自定义拼图库
RefreshCustomPuzzleLibrary();
}
/// <summary>
/// 刷新自定义拼图库
/// </summary>
public void RefreshCustomPuzzleLibrary()
{
string dirPath = Path.Combine(Application.persistentDataPath, "CustomPuzzles");
if (!Directory.Exists(dirPath))
return;
// 获取所有PNG文件
string[] files = Directory.GetFiles(dirPath, "*.png");
// 在这里可以更新自定义拼图列表UI等
Debug.Log($"自定义拼图库中有 {files.Length} 个拼图");
}
/// <summary>
/// 从自定义拼图库加载图像
/// </summary>
public IEnumerator LoadFromCustomPuzzleLibrary(string filename)
{
string fullPath = Path.Combine(Application.persistentDataPath, "CustomPuzzles", filename);
if (!File.Exists(fullPath))
{
Debug.LogError("文件不存在: " + fullPath);
yield break;
}
using (UnityWebRequest request = UnityWebRequestTexture.GetTexture("file://" + fullPath))
{
yield return request.SendWebRequest();
if (request.isNetworkError || request.isHttpError)
{
Debug.LogError("图像加载失败: " + request.error);
}
else
{
Texture2D texture = ((DownloadHandlerTexture)request.downloadHandler).texture;
// 确保纹理可读写
Texture2D readableTexture = MakeTextureReadable(texture);
// 更新预览
if (previewImage != null)
{
previewImage.sprite = Sprite.Create(
readableTexture,
new Rect(0, 0, readableTexture.width, readableTexture.height),
new Vector2(0.5f, 0.5f)
);
}
// 触发事件
OnImageImported?.Invoke(readableTexture);
}
}
}
/// <summary>
/// 获取自定义拼图列表
/// </summary>
public string[] GetCustomPuzzleList()
{
string dirPath = Path.Combine(Application.persistentDataPath, "CustomPuzzles");
if (!Directory.Exists(dirPath))
return new string[0];
return Directory.GetFiles(dirPath, "*.png");
}
}
这个自定义拼图管理器允许玩家从设备导入图片或使用相机拍照作为拼图。它包含了图像处理功能,如裁剪、调整大小,以确保导入的图像适合作为拼图使用。此外,它还提供了自定义拼图库的保存和加载功能,使玩家可以管理自己创建的拼图。
9.6.2 广告与应用内购买
要实现游戏的商业化,我们可以添加广告和应用内购买功能:
csharp
using UnityEngine;
using UnityEngine.Purchasing;
using System.Collections.Generic;
public class PuzzleGameMonetization : MonoBehaviour, IStoreListener
{
// 单例实例
private static PuzzleGameMonetization _instance;
public static PuzzleGameMonetization Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<PuzzleGameMonetization>();
if (_instance == null)
{
GameObject obj = new GameObject("MonetizationManager");
_instance = obj.AddComponent<PuzzleGameMonetization>();
DontDestroyOnLoad(obj);
}
}
return _instance;
}
}
// Unity IAP相关
private IStoreController storeController;
private IExtensionProvider extensionProvider;
// 产品ID
private const string PRODUCT_REMOVE_ADS = "com.puzzlegame.removeads";
private const string PRODUCT_UNLOCK_ALL = "com.puzzlegame.unlockall";
private const string PRODUCT_HINT_PACK = "com.puzzlegame.hintpack";
private const string SUBSCRIPTION_PREMIUM = "com.puzzlegame.premium";
// 广告设置
[SerializeField] private float interstitialAdInterval = 300f; // 5分钟
private float lastInterstitialAdTime = 0f;
// 玩家购买状态
private bool hasRemovedAds = false;
private bool hasUnlockedAll = false;
private int hintCount = 0;
private bool isPremium = false;
// 事件
public delegate void PurchaseSuccessHandler(string productId);
public event PurchaseSuccessHandler OnPurchaseSuccess;
public delegate void AdCompletedHandler(bool success);
public event AdCompletedHandler OnRewardedAdCompleted;
// 初始化
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
// 初始化IAP
InitializeIAP();
// 初始化广告SDK
InitializeAds();
// 加载已购买状态
LoadPurchasedState();
}
/// <summary>
/// 初始化IAP
/// </summary>
private void InitializeIAP()
{
// 如果已经初始化,不需要重复初始化
if (storeController != null)
return;
// 创建构建器并设置产品
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
// 添加产品
builder.AddProduct(PRODUCT_REMOVE_ADS, ProductType.NonConsumable);
builder.AddProduct(PRODUCT_UNLOCK_ALL, ProductType.NonConsumable);
builder.AddProduct(PRODUCT_HINT_PACK, ProductType.Consumable);
builder.AddProduct(SUBSCRIPTION_PREMIUM, ProductType.Subscription);
// 初始化购买系统
UnityPurchasing.Initialize(this, builder);
}
/// <summary>
/// 初始化广告SDK
/// </summary>
private void InitializeAds()
{
// 这里应该根据使用的广告SDK添加相应的初始化代码
// 例如,对于Unity Ads:
// Advertisement.Initialize("YOUR_GAME_ID");
Debug.Log("广告SDK初始化");
}
/// <summary>
/// 加载已购买状态
/// </summary>
private void LoadPurchasedState()
{
hasRemovedAds = PlayerPrefs.GetInt("RemoveAds", 0) == 1;
hasUnlockedAll = PlayerPrefs.GetInt("UnlockAll", 0) == 1;
hintCount = PlayerPrefs.GetInt("HintCount", 0);
isPremium = PlayerPrefs.GetInt("PremiumSubscription", 0) == 1;
}
/// <summary>
/// 保存购买状态
/// </summary>
private void SavePurchasedState()
{
PlayerPrefs.SetInt("RemoveAds", hasRemovedAds ? 1 : 0);
PlayerPrefs.SetInt("UnlockAll", hasUnlockedAll ? 1 : 0);
PlayerPrefs.SetInt("HintCount", hintCount);
PlayerPrefs.SetInt("PremiumSubscription", isPremium ? 1 : 0);
PlayerPrefs.Save();
}
#region IStoreListener接口实现
/// <summary>
/// 初始化完成回调
/// </summary>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
storeController = controller;
extensionProvider = extensions;
Debug.Log("IAP初始化成功");
// 验证已购买的非消耗品
ValidatePurchases();
}
/// <summary>
/// 初始化失败回调
/// </summary>
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.LogError("IAP初始化失败: " + error);
}
/// <summary>
/// 购买失败回调
/// </summary>
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
Debug.LogError($"购买失败: {product.definition.id}, 原因: {failureReason}");
}
/// <summary>
/// 购买完成回调
/// </summary>
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
string productId = args.purchasedProduct.definition.id;
// 处理不同产品的购买
if (string.Equals(productId, PRODUCT_REMOVE_ADS, System.StringComparison.Ordinal))
{
hasRemovedAds = true;
}
else if (string.Equals(productId, PRODUCT_UNLOCK_ALL, System.StringComparison.Ordinal))
{
hasUnlockedAll = true;
}
else if (string.Equals(productId, PRODUCT_HINT_PACK, System.StringComparison.Ordinal))
{
hintCount += 10; // 增加10个提示
}
else if (string.Equals(productId, SUBSCRIPTION_PREMIUM, System.StringComparison.Ordinal))
{
isPremium = true;
hasRemovedAds = true; // 高级订阅包含无广告
}
else
{
Debug.LogWarning("未处理的产品: " + productId);
}
// 保存购买状态
SavePurchasedState();
// 触发事件
OnPurchaseSuccess?.Invoke(productId);
return PurchaseProcessingResult.Complete;
}
#endregion
/// <summary>
/// 验证已购买的非消耗品
/// </summary>
private void ValidatePurchases()
{
if (storeController == null)
return;
// 检查非消耗品
if (storeController.products.WithID(PRODUCT_REMOVE_ADS).hasReceipt)
hasRemovedAds = true;
if (storeController.products.WithID(PRODUCT_UNLOCK_ALL).hasReceipt)
hasUnlockedAll = true;
// 检查订阅
if (storeController.products.WithID(SUBSCRIPTION_PREMIUM).hasReceipt)
{
// 验证订阅是否有效
// 这里需要根据所用平台添加详细的订阅验证代码
isPremium = true;
hasRemovedAds = true;
}
// 保存更新后的状态
SavePurchasedState();
}
/// <summary>
/// 购买产品
/// </summary>
public void PurchaseProduct(string productId)
{
if (storeController == null)
{
Debug.LogError("IAP未初始化");
return;
}
// 查找产品
Product product = storeController.products.WithID(productId);
if (product != null && product.availableToPurchase)
{
Debug.Log($"正在购买: {product.definition.id}");
storeController.InitiatePurchase(product);
}
else
{
Debug.LogError($"产品不可购买: {productId}");
}
}
/// <summary>
/// 显示广告
/// </summary>
public void ShowInterstitialAd()
{
// 如果玩家已移除广告,不显示
if (hasRemovedAds || isPremium)
return;
// 检查时间间隔
float timeSinceLastAd = Time.time - lastInterstitialAdTime;
if (timeSinceLastAd < interstitialAdInterval)
return;
// 显示广告
// 这里应该添加特定广告SDK的代码
// 例如,对于Unity Ads:
// if (Advertisement.IsReady("interstitial"))
// {
// Advertisement.Show("interstitial");
// lastInterstitialAdTime = Time.time;
// }
Debug.Log("显示插页式广告");
lastInterstitialAdTime = Time.time;
}
/// <summary>
/// 显示激励广告
/// </summary>
public void ShowRewardedAd()
{
// 即使玩家已移除广告,也可以选择观看激励广告
// 显示广告
// 这里应该添加特定广告SDK的代码
// 例如,对于Unity Ads:
// if (Advertisement.IsReady("rewardedVideo"))
// {
// var options = new ShowOptions { resultCallback = HandleRewardedAdResult };
// Advertisement.Show("rewardedVideo", options);
// }
Debug.Log("显示激励广告");
// 模拟成功完成广告观看
OnRewardedAdCompleted?.Invoke(true);
}
/// <summary>
/// 处理激励广告结果
/// </summary>
private void HandleRewardedAdResult(bool success)
{
if (success)
{
// 增加提示次数
hintCount += 1;
SavePurchasedState();
}
OnRewardedAdCompleted?.Invoke(success);
}
/// <summary>
/// 使用提示
/// </summary>
public bool UseHint()
{
// 如果玩家是高级会员,不消耗提示次数
if (isPremium)
return true;
if (hintCount > 0)
{
hintCount--;
SavePurchasedState();
return true;
}
return false;
}
/// <summary>
/// 检查关卡是否已解锁
/// </summary>
public bool IsLevelUnlocked(int levelIndex)
{
// 如果玩家已解锁全部关卡或是高级会员,所有关卡都可用
if (hasUnlockedAll || isPremium)
return true;
// 否则,根据游戏进度判断
// 假设每3个关卡需要解锁一次
if (levelIndex % 3 == 0 && levelIndex > 0)
{
return PlayerPrefs.GetInt("LevelUnlock_" + levelIndex, 0) == 1;
}
// 前几关默认解锁
return levelIndex < 3;
}
/// <summary>
/// 解锁关卡
/// </summary>
public void UnlockLevel(int levelIndex)
{
PlayerPrefs.SetInt("LevelUnlock_" + levelIndex, 1);
PlayerPrefs.Save();
}
/// <summary>
/// 获取提示次数
/// </summary>
public int GetHintCount()
{
return hintCount;
}
/// <summary>
/// 检查是否已移除广告
/// </summary>
public bool HasRemovedAds()
{
return hasRemovedAds || isPremium;
}
/// <summary>
/// 检查是否是高级会员
/// </summary>
public bool IsPremiumMember()
{
return isPremium;
}
/// <summary>
/// 添加提示次数(用于测试)
/// </summary>
public void AddHints(int count)
{
hintCount += count;
SavePurchasedState();
}
/// <summary>
/// 获取产品价格
/// </summary>
public string GetProductPrice(string productId)
{
if (storeController == null)
return "N/A";
Product product = storeController.products.WithID(productId);
if (product != null)
{
return product.metadata.localizedPriceString;
}
return "N/A";
}
}
这个货币化管理器实现了广告展示和应用内购买功能,包括非消耗品(移除广告、解锁全部关卡)、消耗品(提示包)和订阅(高级会员)。它与Unity IAP系统集成,处理购买流程和状态管理。此外,它还提供了广告控制功能,如插页式广告和激励广告,以及基于玩家购买状态的功能解锁机制。
9.6.3 社交分享与排行榜
为了增加游戏的社交性和竞争性,我们可以添加分享和排行榜功能:
csharp
using UnityEngine;
using System.Collections;
using System.IO;
using System.Collections.Generic;
public class PuzzleSocialManager : MonoBehaviour
{
// 单例实例
private static PuzzleSocialManager _instance;
public static PuzzleSocialManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<PuzzleSocialManager>();
if (_instance == null)
{
GameObject obj = new GameObject("SocialManager");
_instance = obj.AddComponent<PuzzleSocialManager>();
DontDestroyOnLoad(obj);
}
}
return _instance;
}
}
// 排行榜数据
[System.Serializable]
public class LeaderboardEntry
{
public string playerName;
public int levelIndex;
public int moves;
public float time;
public int stars;
public System.DateTime date;
public LeaderboardEntry(string name, int level, int moveCount, float timeSpent, int starCount)
{
playerName = name;
levelIndex = level;
moves = moveCount;
time = timeSpent;
stars = starCount;
date = System.DateTime.Now;
}
}
// 本地排行榜
private List<LeaderboardEntry> localLeaderboard = new List<LeaderboardEntry>();
// 排行榜排序比较器
private class LeaderboardComparer : IComparer<LeaderboardEntry>
{
public int Compare(LeaderboardEntry a, LeaderboardEntry b)
{
// 首先按星级排序(降序)
int starComparison = b.stars.CompareTo(a.stars);
if (starComparison != 0)
return starComparison;
// 然后按移动次数排序(升序)
int moveComparison = a.moves.CompareTo(b.moves);
if (moveComparison != 0)
return moveComparison;
// 最后按时间排序(升序)
return a.time.CompareTo(b.time);
}
}
// 初始化
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
// 加载本地排行榜
LoadLocalLeaderboard();
}
/// <summary>
/// 加载本地排行榜
/// </summary>
private void LoadLocalLeaderboard()
{
string path = Path.Combine(Application.persistentDataPath, "leaderboard.json");
if (File.Exists(path))
{
try
{
string json = File.ReadAllText(path);
LeaderboardData data = JsonUtility.FromJson<LeaderboardData>(json);
localLeaderboard = data.entries;
Debug.Log("本地排行榜加载成功");
}
catch (System.Exception e)
{
Debug.LogError("加载本地排行榜失败: " + e.Message);
localLeaderboard = new List<LeaderboardEntry>();
}
}
else
{
localLeaderboard = new List<LeaderboardEntry>();
}
}
/// <summary>
/// 保存本地排行榜
/// </summary>
private void SaveLocalLeaderboard()
{
string path = Path.Combine(Application.persistentDataPath, "leaderboard.json");
try
{
LeaderboardData data = new LeaderboardData();
data.entries = localLeaderboard;
string json = JsonUtility.ToJson(data);
File.WriteAllText(path, json);
Debug.Log("本地排行榜保存成功");
}
catch (System.Exception e)
{
Debug.LogError("保存本地排行榜失败: " + e.Message);
}
}
// 用于JSON序列化的包装类
[System.Serializable]
private class LeaderboardData
{
public List<LeaderboardEntry> entries;
}
/// <summary>
/// 提交成绩到本地排行榜
/// </summary>
public void SubmitScore(string playerName, int levelIndex, int moves, float time, int stars)
{
// 创建新条目
LeaderboardEntry entry = new LeaderboardEntry(playerName, levelIndex, moves, time, stars);
// 添加到本地排行榜
localLeaderboard.Add(entry);
// 排序排行榜
localLeaderboard.Sort(new LeaderboardComparer());
// 如果条目过多,保留前100个
if (localLeaderboard.Count > 100)
{
localLeaderboard.RemoveRange(100, localLeaderboard.Count - 100);
}
// 保存排行榜
SaveLocalLeaderboard();
// 如果已集成在线排行榜服务,提交到在线排行榜
SubmitToOnlineLeaderboard(entry);
}
/// <summary>
/// 提交到在线排行榜
/// </summary>
private void SubmitToOnlineLeaderboard(LeaderboardEntry entry)
{
// 这里应该添加特定在线排行榜服务的代码
// 例如,对于Google Play Games:
// Social.ReportScore(entry.moves, "CgkIxxx_leaderboard_moves", (bool success) => {
// Debug.Log("提交成绩 " + (success ? "成功" : "失败"));
// });
Debug.Log("提交在线排行榜成绩");
}
/// <summary>
/// 获取特定关卡的排行榜
/// </summary>
public List<LeaderboardEntry> GetLeaderboard(int levelIndex)
{
// 筛选指定关卡的成绩
List<LeaderboardEntry> levelLeaderboard = localLeaderboard.FindAll(e => e.levelIndex == levelIndex);
// 排序
levelLeaderboard.Sort(new LeaderboardComparer());
return levelLeaderboard;
}
/// <summary>
/// 获取全局排行榜
/// </summary>
public List<LeaderboardEntry> GetGlobalLeaderboard()
{
// 创建排行榜副本并排序
List<LeaderboardEntry> globalLeaderboard = new List<LeaderboardEntry>(localLeaderboard);
globalLeaderboard.Sort(new LeaderboardComparer());
return globalLeaderboard;
}
/// <summary>
/// 分享游戏成绩
/// </summary>
public void ShareScore(int levelIndex, int moves, float time, int stars)
{
// 准备分享文本
string shareText = $"我在《拼图挑战》第{levelIndex + 1}关取得了{stars}星成绩,用了{moves}步,耗时{FormatTime(time)}!来挑战我吧!#PuzzleChallenge";
// 生成截图
StartCoroutine(TakeScreenshotAndShare(shareText));
}
/// <summary>
/// 截图并分享
/// </summary>
private IEnumerator TakeScreenshotAndShare(string text)
{
// 等待帧结束,确保UI渲染完成
yield return new WaitForEndOfFrame();
// 创建纹理并读取屏幕像素
Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
screenshot.Apply();
// 将纹理编码为PNG
byte[] bytes = screenshot.EncodeToPNG();
// 保存截图
string fileName = "puzzle_screenshot_" + System.DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".png";
string filePath = Path.Combine(Application.temporaryCachePath, fileName);
File.WriteAllBytes(filePath, bytes);
// 分享
ShareTextAndImage(text, filePath);
// 清理
Destroy(screenshot);
}
/// <summary>
/// 分享文本和图片
/// </summary>
private void ShareTextAndImage(string text, string imagePath)
{
// 这里应该添加特定平台的分享代码
#if UNITY_ANDROID
// Android分享
AndroidJavaClass intentClass = new AndroidJavaClass("android.content.Intent");
AndroidJavaObject intentObject = new AndroidJavaObject("android.content.Intent");
intentObject.Call<AndroidJavaObject>("setAction", intentClass.GetStatic<string>("ACTION_SEND"));
intentObject.Call<AndroidJavaObject>("setType", "image/png");
intentObject.Call<AndroidJavaObject>("putExtra", intentClass.GetStatic<string>("EXTRA_TEXT"), text);
AndroidJavaClass uriClass = new AndroidJavaClass("android.net.Uri");
AndroidJavaObject fileObject = new AndroidJavaObject("java.io.File", imagePath);
AndroidJavaObject uriObject = uriClass.CallStatic<AndroidJavaObject>("fromFile", fileObject);
intentObject.Call<AndroidJavaObject>("putExtra", intentClass.GetStatic<string>("EXTRA_STREAM"), uriObject);
AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject currentActivity = unity.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject chooser = intentClass.CallStatic<AndroidJavaObject>("createChooser", intentObject, "分享到");
currentActivity.Call("startActivity", chooser);
#elif UNITY_IOS
// iOS分享
// 需要使用插件或本地代码实现
Debug.Log("iOS分享功能需要单独实现");
#else
Debug.Log("当前平台不支持分享功能");
#endif
}
/// <summary>
/// 显示在线排行榜UI
/// </summary>
public void ShowLeaderboardUI()
{
// 这里应该添加特定排行榜服务的代码
// 例如,对于Google Play Games:
// Social.ShowLeaderboardUI();
Debug.Log("显示排行榜UI");
}
/// <summary>
/// 格式化时间
/// </summary>
private string FormatTime(float timeInSeconds)
{
int minutes = Mathf.FloorToInt(timeInSeconds / 60);
int seconds = Mathf.FloorToInt(timeInSeconds % 60);
return string.Format("{0:00}:{1:00}", minutes, seconds);
}
}
这个社交管理器实现了本地排行榜和分享功能。它允许玩家保存和查看自己的游戏成绩,并与朋友分享截图和成绩文本。此外,它还提供了与在线排行榜服务集成的接口,可以根据实际需要添加特定平台的实现代码。
9.7 总结与最佳实践
通过本章的学习,我们已经全面了解了如何在Unity中设计和实现一个功能完整的拼图游戏。从基础的数学模型和拼图可解性理论,到高级的游戏功能和商业化选项,我们涵盖了拼图游戏开发的各个方面。
在实际项目开发中,以下最佳实践值得注意:
-
性能优化:使用对象池和纹理缓存等技术减少内存分配和垃圾回收,特别是在移动平台上。
-
模块化设计:将游戏功能拆分为独立的组件,如拼图生成器、碎片控制器和游戏管理器,使代码更易于维护和扩展。
-
用户体验:提供平滑的动画、有意义的音效和清晰的视觉反馈,增强游戏的沉浸感。
-
可访问性:支持多种输入方式(键盘、鼠标、触摸)和界面选项,使游戏适合不同类型的玩家。
-
多语言支持:从一开始就考虑国际化,使游戏能够轻松适应全球市场。
-
数据管理:实现稳健的存档系统,确保玩家的进度不会丢失,并使用适当的加密保护敏感数据。
-
适度的商业化:平衡游戏的免费体验和付费内容,确保非付费玩家也能够享受游戏的核心乐趣。
拼图游戏是一种经典的游戏类型,它简单易懂但又充满挑战性,适合各种年龄段的玩家。通过添加现代游戏元素,如精美的视觉效果、社交功能和多样化的游戏模式,传统的拼图游戏可以焕发新的生机,为玩家带来持久的乐趣。
希望本章的内容能够帮助你构建自己的拼图游戏,并为你的游戏开发之旅提供有价值的指导和灵感。祝你的拼图游戏开发一切顺利!
这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!
更多推荐


所有评论(0)