Unity小游戏——迷你拼图

 游戏展示

拼图演示

资源: 

链接:https://pan.baidu.com/s/1BGeSmRCO_WZRUyl3MxefGw 
提取码:0n4a


一、玩法介绍

排列拼图碎片,拼出最后的图案。可以点住碎片的任意位置拖动;点击"重来"按钮,可以回到最初状态重新开始。

二、流畅的拖拽操作

有很多电脑游戏的原型来自于现实世界中的玩具,拼图游戏就是其中的一个代表。

本文我们介绍的拼图游戏虽然是一款玩法比较简单的游戏,不过这并不意味着开发也非常简单。

相对于其他游戏通过操作键盘或移动鼠标来控制角色的运动方向,拼图游戏通过鼠标的拖拽直接移动拼图的碎片。

游戏的核心在于流畅的拖拽操作

除了拖拽操作外,我们也可以借此机会思考一下诸如"当碎片移动到正确的位置附近时会被吸附到正确的位置"等触屏游戏的小细节。

三、点住碎片的任意位置拖动

Unity可以很容易判断出"某个对象受到了点击",不过如果要实现自然流畅的操作,我们仍需下功夫。这里,为了让鼠标的拖拽操作更急接近"用手指摁住移动"的效果,我们需要考虑一下如何才能点住碎片的任意位置拖动。

1、透视变换和逆透视变换

鼠标的光标位于屏幕上时,其位置坐标位于二维坐标系内。而拼图碎片位于3D空间内,所以其位置坐标自然有三个维度。为了比较鼠标光标和拼图碎片的位置,必须将它们放入相同的坐标系。因此,我们使用逆透视变换的方法,将鼠标光标的坐标变换至三维坐标系

2、被点击处即为光标的位置

通过逆透视变换将鼠标光标和拼图碎片的坐标统一到相同的坐标系后,我们就该尝试通过拖拽使拼图移动了,只需要在点击按键的瞬间,将鼠标光标的坐标复制到拼图碎片的坐标即可。这种方法确实非常简单,不过它有个缺点:鼠标光标总是显示在拼图碎片的中心。

在本游戏中,拼图碎片被点击的位置并不影响游戏的玩法。不过,对于某些游戏而言,点击位置的不同可能会改变角色的朝向,或者是游戏角色以光标为中心摆动,这些情况下在何处点击就变得很重要了。

而且,即使不影响游戏的核心玩法,点击的瞬间拼图碎片会突然移动一下这种体验也很糟糕。尽管有些时候这种机制可能会更好,但是为了应对不同的要求,我们还是需要掌握如何能点住碎片的任意处拖动。

在本游戏中,碎片的点击判断都是通过Unity的网格碰撞器实现的。网格碰撞器采用网格进行碰撞检测,点击拼图碎片的任何部位都将发生碰撞。对于玩家来说点击碎片的哪个位置都可以,这反映到程序中就是"不用关心碎片的何处受到了点击"。

点击的瞬间,鼠标光标不一定位于碎片的中心。两者的坐标存在一定的差距,我们将这种坐标的差距称为偏移

之前我们把光标的坐标原原本本地复制到碎片坐标时,因为两个坐标值相同所以差距为0,这种坐标差的急剧变化正是导致拼图碎片突然移动的原因。

知道了坐标偏移值的变化是问题所在后,我们来考虑如何固定这个偏移值。首先,要在鼠标点击拼图碎片的瞬间,也就是开始拖动的时候,计算出鼠标光标和碎片中心的坐标差,得到的值就是偏移值。

偏移=碎片的位置-鼠标光标的位置

拖动的过程中则与之相反,用鼠标光标的位置加上偏移值就可以得到碎片的位置

碎片的位置=鼠标光标位置+偏移值

这样一来,鼠标光标距离碎片中心总是保持一定的距离,这样就保证了鼠标点击瞬间的位置就是碎片被拖拽的位置。

下面来看看实际的代码

private void begin_dragging(){do {// 将光标坐标变换为3D空间内的世界坐标Vector3 world_position;if(!this.unproject_mouse_position(out world_position, Input.mousePosition)) {break;}if(PieceControl.IS_ENABLE_GRAB_OFFSET) {// 求出偏移值(点击位置距离碎片的中心有多远)this.grab_offset = this.transform.position - world_position;}} while(false);}
private void do_dragging(){do {// 将光标坐标变换为3D空间内的世界坐标Vector3 world_position;if(!this.unproject_mouse_position(out world_position, Input.mousePosition)) {break;}// 加上光标坐标(3D)的偏移值,计算出碎片的中心坐标this.transform.position = world_position + this.grab_offset;} while(false);}

四、打乱拼图碎片

商店里售卖纸质拼图游戏时一般会将拼图碎片打乱顺序后放入包装盒中。虽然也有些是已经拼好的状态,不过玩家在开始游戏之前还是要将各碎片的顺序打乱。

有很多事情都是"人类做起来很简单,计算机处理起来却很难",比如将拼图碎片全部打乱这件事就是一个例子。

Unity提供了取得随机数的方法,不过单纯使用该方法似乎并不能达到打乱碎片顺序的目的。

这里我们不妨来分析一下如何随机打乱各拼图碎片的顺序。

1、设置拼图碎片的坐标为随机数

最简单的随机打乱拼图碎片的方法是,直接将随机数代入各个碎片的坐标。只要控制好随机数的范围,就能让各个拼图碎片随机分布在画面上。

但这种方式的弊端也很明显,就是有很多拼图碎片可能叠在一起。虽然这样也未尝不可,不过可以的话最好还是将各碎片均匀分散开。如果很多碎片重叠在一起,就可能导致下面的碎片被覆盖而无法看见。

2、改进策略

首先我们整理一下拼图碎片随机散开的要求,即需求分析

  • 碎片之间彼此互不重叠
  • 碎片散开分布到整个画面上
  • 随机分散各个碎片

需求基本上就是这样。如果拼图碎片的数量有所增加,可能还需要追加一项"能够控制游戏的难易度"。

接下来我们对实现方法进行说明。首先简单熟悉一下整体流程

  1. 将拼图碎片分配到网格中
  2. 打乱拼图碎片的排列顺序
  3. 在网格内通过随机坐标调整碎片的位置
  4. 将整个拼图随机旋转一定角度

我们可以选择任意图片,将其分割成几块。这里我们选择一个"猫头鹰"图片,将其分割成8块。

首先,将所有的拼图碎片从左上角开始依次放入网格中。

该网格的行数和列数相同,并且网格总数达于拼图碎片数量。"猫头鹰"拼图碎片数量为8,我们就搞一个3✖️3的网格,空出来的格子不用理会。根据碎片数量的不同,有时候剩余的格子会比较多,这种情况下可以调整网格的行数和列数。

所有网格块都为正方形,且都应当能确保能够容纳下拼图碎片。另外,因为后续步骤在网格内移动拼图碎片,所以还需要在确保整体网格不溢出画面的前提下适当放大网格的尺寸。

之所以想这样把拼图碎片纺织到网格中,是为了避免出现碎片之间彼此重叠的状况。

接下来随机打乱个碎片的排列位置

在第一个步骤中,我们将碎片从左上角开始依次放入了网格中。而第二个步骤就是打乱各个碎片的排列顺序。利用随机数选出两个网格,案后交换其中的碎片空白的网格也可以参与交换。

做到这里,前面我们做出的需求分析中,"碎片之间彼此互不重叠","碎片分散于整个画面"和"随机分散各个碎片"就已经基本得到了实现。不过从程序实际情况来看,很容易发现拼图碎片被规则地排列在了网格上。我们得想办法让这种随机分散的效果更真实。

在第三个步骤中,我们让拼图碎片在网格中随机移动。

最初的步骤中增加网格尺寸的用意就在于为这里的碎片移动做准备。如果网格的尺寸太小,将无法移动碎片,反之如果太大,则会令碎片之间过于松散。我们需要结合拼图碎片的大小和画面整体的尺寸,调整网格尺寸为最佳值,

最后,为了不让玩家看出碎片排列的规律性,稍微将拼图网格整体旋转一定的角度

虽然旋转了整体的网格,但是需要保持拼图碎片自身的角度不变。

下面,我们结合实际代码来看看这一流程

private void	shuffle_pieces(){#if true// 将碎片按照网格顺序排列int[]		piece_index = new int[this.shuffle_grid_num*this.shuffle_grid_num];for(int i = 0;i < piece_index.Length;i++) {if(i < this.all_pieces.Length) {piece_index[i] = i;} else {piece_index[i] = -1;}}// 随机选取两个碎片,交换位置for(int i = 0;i < piece_index.Length - 1;i++) {int	j = Random.Range(i + 1, piece_index.Length);int	temp = piece_index[j];piece_index[j] = piece_index[i];piece_index[i] = temp;}// 通过位置的索引变换为实际的坐标来进行配置Vector3	pitch;pitch = this.shuffle_zone.size/(float)this.shuffle_grid_num;for(int i = 0;i < piece_index.Length;i++) {if(piece_index[i] < 0) {continue;}PieceControl	piece = this.all_pieces[piece_index[i]];Vector3	position = piece.finished_position;int		ix = i%this.shuffle_grid_num;int		iz = i/this.shuffle_grid_num;position.x = ix*pitch.x;position.z = iz*pitch.z;position.x += this.shuffle_zone.center.x - pitch.x*(this.shuffle_grid_num/2.0f - 0.5f);position.z += this.shuffle_zone.center.z - pitch.z*(this.shuffle_grid_num/2.0f - 0.5f);position.y = piece.finished_position.y;piece.start_position = position;}// 逐步(网格的格子内)随机移动位置Vector3		offset_cycle = pitch/2.0f;Vector3		offset_add   = pitch/5.0f;Vector3		offset = Vector3.zero;for(int i = 0;i < piece_index.Length;i++) {if(piece_index[i] < 0) {continue;}PieceControl	piece = this.all_pieces[piece_index[i]];Vector3	position = piece.start_position;position.x += offset.x;position.z += offset.z;piece.start_position = position;//offset.x += offset_add.x;if(offset.x > offset_cycle.x/2.0f) {offset.x -= offset_cycle.x;}offset.z += offset_add.z;if(offset.z > offset_cycle.z/2.0f) {offset.z -= offset_cycle.z;}}// 使全体旋转foreach(PieceControl piece in this.all_pieces) {Vector3		position = piece.start_position;position -= this.shuffle_zone.center;position = Quaternion.AngleAxis(this.pazzle_rotation, Vector3.up)*position;position += this.shuffle_zone.center;piece.start_position = position;}this.pazzle_rotation += 90;#else// 简单地使用随机数来决定坐标时的情况foreach(PieceControl piece in this.all_pieces) {Vector3	position;Bounds	piece_bounds = piece.GetBounds(Vector3.zero);position.x = Random.Range(this.shuffle_zone.min.x - piece_bounds.min.x, this.shuffle_zone.max.x - piece_bounds.max.x);position.z = Random.Range(this.shuffle_zone.min.z - piece_bounds.min.z, this.shuffle_zone.max.z - piece_bounds.max.z);position.y = piece.finished_position.y;piece.start_position = position;}#endif}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/24061.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python:基于Kalman滤波器的移动物体位置估计

CSDN@_养乐多_ Kalman滤波器是一种经典的估计方法,广泛应用于估计系统状态的问题。本篇博客将介绍Kalman滤波器的基本原理,并通过一个简单的Python代码示例,演示如何使用Kalman滤波器来估计移动物体的位置。 通过运行代码,我们将得到一个包含两个子图的图像,分别展示了估…

第二十二篇:思路拓展:如何打造高性能的 React 应用?

React 应用也是前端应用&#xff0c;如果之前你知道一些前端项目普适的性能优化手段&#xff0c;比如资源加载过程中的优化、减少重绘与回流、服务端渲染、启用 CDN 等&#xff0c;那么这些手段对于 React 来说也是同样奏效的。 不过对于 React 项目来说&#xff0c;它有一个区…

Ubuntu 23.04 作为系统盘的体验和使用感受

1.为啥主系统装了Ubuntu 由于公司发电脑了&#xff0c;我自己也有一台台式电脑&#xff0c;然后也想去折腾一下Ubuntu&#xff0c;就把自己的笔记本装成Ubuntu系统了&#xff0c; 我使用的是23.04的桌面版&#xff0c;带图形化界面的。我准备换回Windows 11了&#xff08;因为…

策略模式(Strategy)

策略模式是一种行为设计模式&#xff0c;就是定义一系列算法&#xff0c;然后将每一个算法封装起来&#xff0c;并使它们可相互替换。本模式通过定义一组可相互替换的算法&#xff0c;实现将算法独立于使用它的用户而变化。 Strategy is a behavioral design pattern that def…

Redis 如何解决缓存雪崩、缓存击穿、缓存穿透难题

前言 Redis 作为一门热门的缓存技术&#xff0c;引入了缓存层&#xff0c;就会有缓存异常的三个问题&#xff0c;分别是缓存击穿、缓存穿透、缓存雪崩。我们用本篇文章来讲解下如何解决&#xff01; 缓存击穿 缓存击穿: 指的是缓存中的某个热点数据过期了&#xff0c;但是此…

React Native获取手机屏幕宽高(Dimensions)

import { Dimensions } from react-nativeconsole.log(Dimensions, Dimensions.get(window)) 参考链接&#xff1a; https://www.reactnative.cn/docs/next/dimensions#%E6%96%B9%E6%B3%95 https://chat.xutongbao.top/

Python3 处理PDF之PyMuPDF 入门

PyMuPDF 简介 PyMuPDF是一个用于处理PDF文件的Python库&#xff0c;它提供了丰富的功能来操作、分析和转换PDF文档。这个库的设计目标是提供一个简单易用的API,使得开发者能够轻松地在Python程序中实现PDF文件的各种操作。 PyMuPDF的主要特点如下&#xff1a; 跨平台兼容性&a…

C++20 协程(coroutine)入门

文章目录 C20 协程&#xff08;coroutine&#xff09;入门什么是协程无栈协程和有栈协程有栈协程的例子例 1例 2 对称协程与非对称协程无栈协程的模型无栈协程的调度器朴素的单线程调度器让协程学会等待Python 中的异步函数可等待对象M:N 调度器——C# 中的异步函数 小结 C20 中…

替换开源LDAP,西井科技用宁盾目录统一身份,为业务敏捷提供支撑

客户介绍 上海西井科技股份有限公司成立于2015年&#xff0c;是一家深耕于大物流领域的人工智能公司&#xff0c;旗下无人驾驶卡车品牌Q-Truck开创了全球全时无人驾驶新能源商用车的先河&#xff0c;迄今为止已为全球16个国家和地区&#xff0c;120余家客户打造智能化升级体验…

JAVA工程师程序员常见的面试题业务场景包含答案【基础,经典,少见,热频,技巧】

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》采购供应链共享平台人员,财务规则对账人员,物流门禁计量系统对接人员,ERP事业部人员 公众号:山峯草堂,非技术多篇文章,专注于天道酬勤的 Java 开发问题、中国国学、传统文化和代码爱好者的程序…

SNAT和DNAT原理与应用

iptables的备份和还原 1.写在命令行当中的都是临时配置。 2.把我们的规则配置在 备份&#xff08;导出&#xff09;&#xff1a;iptables-save > /opt/iptables.bak 默认配置文件&#xff1a;/etc/sysconfig/iptables 永久配置&#xff1a;cat /opt/iptables.bak > /etc…

并查集练习—省份数量

上一篇中讲了并查集及其原理&#xff0c;在这篇文章中简单应用一下。如果对并查集不是很了解强烈建议先看上一篇。 题目&#xff1a; 有 n 个城市&#xff0c;其中一些彼此相连&#xff0c;另一些没有相连。如果城市 a 与城市 b 直接相连&#xff0c;且城市 b 与城市 c 直接相…

DP-GAN损失

在前面我们看了生成器和判别器的组成。 生成器损失公式&#xff1a; 首先将fake image 和真实的 image输入到判别器中&#xff1a; 接着看第一个损失&#xff1a;参数分别为fake image经过判别器的输出mask&#xff0c;和真实的label进行损失计算。对应于&#xff1a; 其中l…

【2023】字节跳动 10 日心动计划——第六关

目录 1. 环形链表 II2. 有序数组中的单一元素 1. 环形链表 II &#x1f517; 原题链接&#xff1a;142. 环形链表 II 用哈希表判重即可。 class Solution { public:ListNode *detectCycle(ListNode *head) {unordered_set<ListNode*> st;while (head) {if (st.count(hea…

捕捉时刻:将PDF文件中的图像提取为个性化的瑰宝(从pdf提取图像)

应用场景&#xff1a; 该功能的用途是从PDF文件中提取图像。这在以下情况下可能会很有用&#xff1a; 图片提取和转换&#xff1a;可能需要将PDF文件中的图像提取出来&#xff0c;并保存为单独的图像文件&#xff0c;以便在其他应用程序中使用或进行进一步处理。例如&#xff…

恺英网络宣布:与华为鸿蒙系统展开合作,将开发多款手游

8月5日消息&#xff0c;恺英网络宣布旗下子公司盛和网络参加了华为开发者大会&#xff08;HDC.Together&#xff09;游戏服务论坛&#xff0c;并在华为鸿蒙生态游戏先锋合作启动仪式上进行了亮相。恺英网络表示&#xff0c;将逐步在HarmonyOS上开发多款游戏&#xff0c;利用Har…

JVM 调优

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ JVM调优是一项重要的任务&#xff0c;可以提高Java应用程序的性能和稳定性。掌握JVM调优需要深入了解JVM的工作原理、参数和配置选项&#xff0c;以及历史JVM参数的调整和优…

WMS仓库管理系统研发规划说明

01 产品背景 1.1 背景概述 aboss WMS东南亚仓库管理系统是一个基于BigSeller系统的使用基础上&#xff0c;加上多仓库的解决思路&#xff0c;解决入库业务、出库业务、仓库调拨、库存调拨和虚仓管理等功能&#xff0c;对批次管理、物料对应、库存盘点、质检管理、虚仓管理和即…

QT 杂项笔记qobject_cast

1、使用qobject_cast进行安全类型转换 int main(int argc, char *argv[]) {QApplication a(argc, argv);QObject *btn new QPushButton;qDebug() << btn->metaObject()->className(); //查看类名//使用qobject进行安全类型转换&#xff0c;转换目标为QPushButton…

MYSQL进阶-事务的基础知识

1.什么是数据库事务&#xff1f; 就是把好几个sql语句打包成一个整体执行&#xff0c;要么全部成功&#xff0c;要么全部失败&#xff01;&#xff01;&#xff01; 事务是一个不可分割的数据库操作序列&#xff0c;也是数据库并发控制的基本单位&#xff0c;其执 行的结果必…