Unity开发 第四章 贪吃蛇小游戏制作半完成版
Unity开发 第一章 认识模板和界面以及项目导入到Rider
Unity开发 第二章 2D游戏制作之文件结构以及素材利用(主要是脚本绑定)
Unity开发 第三章 浮动的按钮以及第一个小游戏
Unity开发 第四章 贪吃蛇小游戏制作半完成版
前言
贪吃蛇还是有点难度的,学习这次的游戏开发经验可以很好的完善自己对Unity的一个认识。
根据上一篇博客的铺垫,可以了解到蛇头的移动逻辑,但是移动逻辑最主要的应该是蛇身的移动和生成逻辑。
首先,我先展示本期开发完的效果
完成的功能主要有移动逻辑、食物的碰撞逻辑、环境的随机生成食物的逻辑
我们先略过上一期提到的一些素材,我发现素材还是用简单点好
一、素材收集
一样的
蛇头和食物、背景就行
如果想知道怎么把素材放到Unity,建议去看看上一期的开发内容
然后就是素材和代码的结合了
- 首先背景自己随便找个UI的Image组件给放进去
- 其次是食物,建立一个GameObject,添加组件,然后自己看图片
- 然后把东西拖到预制体文件夹里(随便你创的文件夹)
然后是蛇身体和蛇头的制作
蛇头的制作和食物是一样的,只是要不要预制体都一样,反正我是直接GameObject加Render组件一丢
然后是制作一个Body的组件,上次自己画的一点也不好用
然后自己设置一下,图层和我一样也是行的,免得看不见,食物,蛇全部都是4图层,以及图层是Default
然后按照我的项目结果来做
Snake是一个GameObject换个名字,下面是创建的子组件,SnakeHead就是这里面的,然后就是脚本内容
二、代码实现内容
接下来对每一个代码都会有分析,毕竟用到了大量的Unity的知识。
首先是移动逻辑,咱们移动的逻辑是放到Snake这个父组件上的
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
namespace Games.games.Gluttonous_Snake
{
public class Snake : MonoBehaviour
{
// SnakeBody节点,代表链表的每一节蛇身
private class SnakeBodyNode
{
public GameObject BodyPart; // 对应蛇身的GameObject
public SnakeBodyNode Next; // 指向下一个蛇身部分
public SnakeBodyNode Prev;
// 此时的运动方向
public Vector2 Direction;
public float Angle;
public Vector3 Position;
public SnakeBodyNode(GameObject part, Vector2 direction)
{
BodyPart = part;
Next = null;
Prev = null;
Direction = direction;
}
}
// 私有变量
private GameObject _snakeHead;
private SnakeBodyNode _snakeBodyHead; // 链表的头节点,指向蛇身的第一个节点
private SnakeBodyNode _snakeBodyEnd;
private Vector2 _direction = Vector2.up; // 蛇的初始移动方向
private Vector2 _lastDirection; // 上一帧的方向
private float _angle;
private bool _ischange = false;
// 公有变量
public float speed = 10f;
public GameObject snakeBody;
public int numberOfSnakes = 3;
public float thresholdDistance = 2f;
void Start()
{
InitSnake();
}
void Update()
{
HandInput();
MoveSnake();
}
void InitSnake()
{
transform.localScale = Vector3.one;
_snakeHead = transform.Find("SnakeHead").gameObject;
// 创建双向链表的头节点
_snakeBodyHead = new SnakeBodyNode(_snakeHead, _direction);
_snakeBodyEnd = new SnakeBodyNode(transform.Find("Body").gameObject, _direction);
_snakeBodyHead.Next = _snakeBodyEnd;
_snakeBodyEnd.Prev = _snakeBodyHead;
// 假设蛇的初始身体只有一个部分,可以根据实际情况修改
for (int i = 0; i < numberOfSnakes; i++)
{
GrowSnake();
}
}
void HandInput()
{
if (Input.GetKeyDown(KeyCode.W) && _direction != Vector2.down && _direction != Vector2.up)
{//向上
_direction = Vector2.up; // 向上(0,-1)
_angle = 0f;
_ischange = true;
}
else if (Input.GetKeyDown(KeyCode.S) && _direction != Vector2.down && _direction != Vector2.up)
{//向下
_direction = Vector2.down; // 向上(0,-1)
_angle = 180f;
_ischange = true;
}
else if (Input.GetKeyDown(KeyCode.D) && _direction != Vector2.right && _direction != Vector2.left)
{//向右
_direction = Vector2.right; // 向上(0,-1)
_angle = -90f;
_ischange = true;
}
else if (Input.GetKeyDown(KeyCode.A) && _direction != Vector2.right && _direction != Vector2.left)
{//向左
_direction = Vector2.left; // 向上(0,-1)
_angle = 90f;
_ischange = true;
}
}
private void RotateSnake(Transform transform1,float angle) { // 旋转蛇头,根据新的方向设置旋转角度
transform1.rotation = Quaternion.Euler(0, 0, angle);
} // 旋转蛇头,Z轴旋转
private void MoveSnake()
{
SnakeBodyNode p = _snakeBodyHead;
if (_ischange)
{
p.Direction = _direction;
p.Angle = _angle;
p.Position = p.BodyPart.transform.position;
_ischange = false;
}
while (true)
{
if (p == null)
{
break;
}
Move(p);
p = p.Next;
}
}
private void Move(SnakeBodyNode p)
{
// 确保蛇头朝着当前方向移动
if (p == _snakeBodyHead)
{
// 对于蛇头,直接用它的当前方向来移动
RotateSnake(p.BodyPart.transform, p.Angle);
p.BodyPart.transform.Translate(p.Direction * (speed * Time.deltaTime), Space.World);
}
else
{
// 计算从当前元素到上一个元素的方向向量
Vector3 directionToPrev = p.Prev.BodyPart.transform.position - p.BodyPart.transform.position;
float distance = directionToPrev.magnitude;
// 根据距离施加斥力或引力
Vector3 forceDirection;
float forceMagnitude;
if (distance < thresholdDistance)
{
// 斥力:方向相反,大小与距离成反比
forceDirection = -directionToPrev.normalized;
forceMagnitude = (thresholdDistance - distance) / thresholdDistance;
}
else
{
// 引力:方向相同,大小与距离成正比
forceDirection = directionToPrev.normalized;
forceMagnitude = 2*(distance - thresholdDistance) / thresholdDistance;
}
// 计算最终的移动方向和速度
Vector3 moveDirection = forceDirection * forceMagnitude;
p.BodyPart.transform.Translate(moveDirection * (speed * Time.deltaTime), Space.World);
}
}
public void GrowSnake()
{
GameObject body = Instantiate(snakeBody, _snakeBodyEnd.BodyPart.transform.position, Quaternion.identity,transform);
Vector3 direction3D = new Vector3(-1*_snakeBodyEnd.Direction.x, -1*_snakeBodyEnd.Direction.y, 0);
SnakeBodyNode p = new SnakeBodyNode(body, _snakeBodyEnd.Direction);
p.Angle = _snakeBodyEnd.Angle;
_snakeBodyEnd.Prev.Next = p;
p.Prev = _snakeBodyEnd.Prev;
p.Next = _snakeBodyEnd;
_snakeBodyEnd.Prev = p;
_snakeBodyEnd.BodyPart.transform.position = p.BodyPart.transform.position + direction3D * thresholdDistance;
}
}
}
这里呢,主要实现是双向链表,以及移动时会计算上一个节点的坐标和本节点之间的方向以及距离实现一个越近就速度越慢,越远就速度越快的内容,可以有效防止蛇身体的粘合,要理解的话建议看看数据结构和逻辑就好
脚本上的预制件需要绑定
这个就是我们身体的移动逻辑
然后是食物的碰撞逻辑以及食物生成逻辑
食物生成逻辑还好说
创建一个Environment的GameObject来绑定脚本
using UnityEngine;
namespace Games.games.Gluttonous_Snake
{
public class Environment : MonoBehaviour
{
public Vector3 environmentSize = new Vector3(100f,100f,0f); // 环境的尺寸(自定义)
public Vector3 environmentCenter; // 环境的中心(基于物体的位置)
public GameObject prefab;
private float _nextSpawnTime = 0;
private float _spawnInterval = 10f;
private GameObject _currentPrefabInstance; // 当前生成的预制体实例
private void Start()
{
// 初始化环境的尺寸和中心
InitEnvironment();
if (prefab != null)
{
// 立即生成第一个预制体
SpawnPrefab();
}
}
private void Update()
{
// 检查是否到了生成预制体的时间
if (Time.time >= _nextSpawnTime)
{
// 移除旧的预制体
RemovePrefab();
// 生成新的预制体
SpawnPrefab();
// 更新下一次生成预制体的时间
_nextSpawnTime = Time.time + _spawnInterval;
}
}
public void SpawnPrefab()
{
Vector3 spawnPosition = new Vector3(
Random.Range(environmentCenter.x - environmentSize.x / 2+1f, environmentCenter.x + environmentSize.x / 2-1f),
Random.Range(environmentCenter.y - environmentSize.y / 2+1f, environmentCenter.y + environmentSize.y / 2-1f),
Random.Range(environmentCenter.z - environmentSize.z / 2+1f, environmentCenter.z + environmentSize.z / 2-1f)
);
// 创建食物实例
_currentPrefabInstance = Instantiate(prefab, spawnPosition, Quaternion.identity);
}
public void RemovePrefab()
{
// 移除当前生成的预制体实例
Destroy(_currentPrefabInstance);
_currentPrefabInstance = null;
}
private void InitEnvironment()
{
// 获取物体的中心位置(环境物体的位置)
environmentCenter = transform.position;
// 手动设置环境尺寸,而不是依赖 localScale
// 你可以根据需求修改这些数值
// 假设环境的尺寸为10x5x10
// 可选: 调试输出环境的中心和尺寸
Debug.Log("Environment Center: " + environmentCenter);
Debug.Log("Environment Size: " + environmentSize);
}
private void OnDrawGizmos()
{
// 如果环境尺寸还没有初始化,则提前退出
if (environmentSize == Vector3.zero) return;
// 设置 Gizmos 的颜色
Gizmos.color = Color.green;
// 绘制一个立方体,表示生成范围
// 这里绘制的是基于环境物体中心的长方体
Gizmos.DrawWireCube(environmentCenter, environmentSize);
}
}
}
RemovePrefab是移除食物Object的函数
InitEnvironment是初始化环境的东西
SpawnPrefab是生成食物在那个范围的函数
OnDrawGizmos是在下面这个模式中可以看到食物生成的范围的函数
那个绿色的就是小边框,下面那两个东西用来调整边框的大小和位置
然后环境这边的食物生成就做好了,预制件那个自己选择自己的食物就可以了
最后是食物与吃西瓜的蛇的碰撞逻辑
下面是我们要做好的准备工作
一是碰撞体,我们先设置蛇头的碰撞体,也就是
把里面的一些参数给去掉
特别是重力
这个组件主要是用来模拟物理世界的一些规则的
第二是给再加一个盒子碰撞体,设置它是触发器
然后我们接着是给食物也加一个碰撞体,然后图层一定要设置好
接着咱们给食物这个预制体打上一个标签
把标签定为“Food”
这个在我们写碰撞体逻辑的时候有用
using Games.games.Gluttonous_Snake;
using UnityEngine;
public class SnakeHead : MonoBehaviour
{
public static string foodTag = "Food"; // 标签用于识别食物预制体
private Snake snakeScript; // 引用Snake脚本
private Environment environmentScript; // 引用Environment脚本
private void Start()
{
// 在这里获取Snake脚本的引用,假设Snake脚本绑定在蛇头的父组件上
snakeScript = transform.parent.GetComponent<Snake>();
GameObject environmentObject = GameObject.Find("Environment");
if (environmentObject != null)
{
environmentScript = environmentObject.GetComponent<Environment>();
}
else
{
Debug.LogError("Environment GameObject not found!");
}
}
void OnTriggerEnter2D(Collider2D other)
{
Debug.Log("发生碰撞");
// 检查碰撞的物体是不是食物
if (other.CompareTag(foodTag)) // 确保你的食物对象有 "Food" 标签
{
Debug.Log("蛇头碰到了食物!");
// 这里可以添加其它处理逻辑,例如销毁食物、增加分数等
snakeScript.GrowSnake();
if (environmentScript != null)
{
environmentScript.RemovePrefab(); // 假设Environment脚本中有一个SpawnFood方法
environmentScript.SpawnPrefab();
}
}
}
}
这个代码的作用是,
- 把父组件同级是Snake组件中脚本的方法给弄过来,搞回调来便于执行蛇生长的逻辑。
- 把父组件同级的Environment组件中的脚本给弄过来,搞回调来便于执行食物随机生成和销毁的逻辑。
- OnTriggerEnter2D这个是当2D组件发生有组件进入碰撞体时,会看看那个组件的名字other的标签是不是和本类定义的foodTag一样
- 最后就完成了食物随机生成和蛇生长与移动,以及食物销毁的逻辑了
总结
本次完成的贪吃蛇已经初具游戏雏形,下次的贪吃蛇写游戏的弹出UI和结束游戏判定以及剩下的积分计算等
给大家表演一个西瓜蛇转圈圈