文章目录
前言
我们将在这个视频中,学习如何在Unity中制作《炸弹人心》,《炸弹人》是---个游戏系列,最初于1983年7月在日本发行,《炸弹人》的游戏玩法包括策略性地放炸弹,在一定时间后以多个方向爆炸,以摧毁障碍物和杀死敌人。
本文重点介绍了实现瓦片地图精灵动画的方法,你可以用许多不同的方式自定义游戏,
想出独特的游戏模式并建立自己的关卡。
先来看看实现的最终效果
素材
开始
一、绘制地图
使用tilemap绘制地图,其实挺简单的,这里我就不再介绍如何使用了,节省大家时间,tilemap还不会用的,可以看我之前的tilemap文章:【Unity小技巧】Unity2D TileMap的探究(最简单,最全面的TileMap使用介绍)
绘制的最终效果,你也可以按自己的喜欢绘制不同的地图
二、玩家设置
给玩家添加刚体和碰撞器,重力设置为0,注意添加圆形的碰撞器,这样可以有效防止角色转弯时卡在墙角
新建2d物理材质,设置角摩擦力和弹力为0,防止角色卡墙
刚体和碰撞器都挂载刚才的2d物理材质
三、玩家移动
Player代码
csharp
public class Player : MonoBehaviour
{
Rigidbody2D rb;
Vector2 movement;
private float horizontalInput;
private float verticalInput;
public float speed;//移动速度
private void Start()
{
rb = GetComponent<Rigidbody2D>();
}
void Update()
{
horizontalInput = Input.GetAxisRaw("Horizontal");
verticalInput = Input.GetAxisRaw("Vertical");
movement = new Vector2(horizontalInput, verticalInput).normalized;
}
private void FixedUpdate()
{
//移动代码
rb.MovePosition(rb.position + movement * speed * Time.fixedDeltaTime);
}
}
运行效果
四、玩家四方向动画运动切换
这里我们使用2D混合动画实现(2D Simple Directional),混合动画的基础使用我之前有说过,不懂得可以回去先看:
零基础带你从小白到超神27------混合状态,混合动画,动画分类
Run混合动画配置(down动画片段是默认人物站立面向观众)
修改人物移动代码
csharp
//控制动画
animator.SetFloat("Horizontal", horizontalInput);
animator.SetFloat("Vertical", verticalInput);
运行效果
问题
如果你的游戏对细节要求不高,其实到这里就已经算完成了。
但是我本着严谨的态度,会发现人物移动停止时都会面向屏幕(也就是前面的down站立动画),这显然不符合逻辑,我们希望人物最终停止面向对应的位置
那么要如何做呢?方法其实有很多,最简单的方法呢,就是在Run混合动画前面再加一个Idle混合动画
我们通过isRun参数来控制动画的切换
Run混合动画配置(所有动画片段都是人物不同方向的站立动画)
修改代码
csharp
//控制动画
if (horizontalInput == 0 && verticalInput == 0){
animator.SetBool("isRun", false);
} else {
animator.SetBool("isRun", true);
animator.SetFloat("Horizontal", horizontalInput);
animator.SetFloat("Vertical", verticalInput);
}
效果
五、放置炸弹
炸弹控制脚本
csharp
using System.Collections;
using UnityEngine;
public class Bomb : MonoBehaviour
{
[Header("Bomb")]
private KeyCode inputKey = KeyCode.Space; // 输入的按键
public GameObject bombPrefab; // 炸弹预制体
public float bombFuseTime = 3f; // 炸弹引线时间
public int bombAmount = 1; // 炸弹数量
private int bombsRemaining; // 剩余炸弹数量
private void OnEnable()
{
bombsRemaining = bombAmount; // 初始化剩余炸弹数量
}
private void Update()
{
if (bombsRemaining > 0 && Input.GetKeyDown(inputKey)) // 如果还有剩余炸弹且按下了指定按键
{
StartCoroutine(PlaceBomb()); // 放置炸弹
}
}
private IEnumerator PlaceBomb()
{
Vector2 position = transform.position; // 获取当前位置
position.x = Mathf.Round(position.x)-0.5f; // 四舍五入x坐标-0.5偏移量
position.y = Mathf.Round(position.y)-0.5f; // 四舍五入y坐标-0.5偏移量
GameObject bomb = Instantiate(bombPrefab, position, Quaternion.identity); // 实例化炸弹
bombsRemaining--; // 剩余炸弹数量减一
yield return new WaitForSeconds(bombFuseTime); // 等待炸弹引线时间
Destroy(bomb.gameObject); // 销毁炸弹游戏对象
bombsRemaining++; // 剩余炸弹数量加一
}
//炸弹默认是触发器 角色离开时开启碰撞效果
private void OnTriggerExit2D(Collider2D other)
{
if (other.gameObject.layer == LayerMask.NameToLayer("Bomb")) // 如果离开触发器的物体属于Bomb层
{
other.isTrigger = false; // 取消触发器属性
}
}
}
角色绑定脚本,配置参数,记得Bomb预制体开启触发器,并指定图层为Bomb
运行效果
六、生成爆炸效果
新增爆炸效果代码
csharp
[Header("爆炸")]
public GameObject explosionEnd; // 爆炸结束
public GameObject explosionMiddle; // 爆炸中间
public GameObject explosionStart; // 爆炸结束
public int explosionRange;//爆炸范围
//生成爆炸效果
public void createExplosion(Vector2 position)
{
//爆炸中心
GameObject explosionStartData = Instantiate(explosionStart, position, Quaternion.identity);
Destroy(explosionStartData, 0.5f);
for (int i = 1; i <= explosionRange; i++)
{
ClearDestructible(new Vector2(position.x + i, position.y), i, 0);
ClearDestructible(new Vector2(position.x - i, position.y), i, 180);
ClearDestructible(new Vector2(position.x, position.y + i), i, 90);
ClearDestructible(new Vector2(position.x, position.y -i), i, -90);
}
}
private bool ClearDestructible(Vector2 position, int i, int rotate)
{
//是不是最后爆炸区
if (i == explosionRange)
{
GameObject explosionEndData = Instantiate(explosionEnd, position, Quaternion.identity);
//设置爆炸效果的方向
explosionEndData.transform.eulerAngles = new Vector3(0, 0, rotate);
Destroy(explosionEndData, 0.5f);
}
else
{
GameObject explosionMiddleData = Instantiate(explosionMiddle, position, Quaternion.identity);
//设置爆炸效果的方向
explosionMiddleData.transform.eulerAngles = new Vector3(0, 0, rotate);
Destroy(explosionMiddleData, 0.5f);
}
return true;
}
效果
七、墙壁和可破坏障碍物的判断
修改代码
csharp
[Header("爆炸")]
public Tilemap wallTileMap; // 可破坏物墙壁的Tilemap组件
public GameObject explosionEnd; // 爆炸结束预制体
public GameObject explosionMiddle; // 爆炸中间预制体
public GameObject explosionStart; // 爆炸结束预制体
public GameObject brickWall;//破坏的墙
public int explosionRange;//爆炸范围
public LayerMask explosionLayerMask; // 墙壁层级
//生成爆炸效果
public void createExplosion(Vector2 position)
{
//爆炸中心
GameObject explosionStartData = Instantiate(explosionStart, position, Quaternion.identity);
Destroy(explosionStartData, 0.5f);
for (int i = 1; i <= explosionRange; i++)
{
bool res = ClearDestructible(new Vector2(position.x + i, position.y), i, 0);
if (!res) break;
}
for (int i = 1; i <= explosionRange; i++)
{
bool res = ClearDestructible(new Vector2(position.x - i, position.y), i, 180);
if (!res) break;
}
for (int i = 1; i <= explosionRange; i++)
{
bool res = ClearDestructible(new Vector2(position.x, position.y + i), i, 90);
if (!res) break;
}
for (int i = 1; i <= explosionRange; i++)
{
bool res = ClearDestructible(new Vector2(position.x, position.y - i), i, -90);
if (!res) break;
}
}
private bool ClearDestructible(Vector2 position, int i, int rotate)
{
if (Physics2D.OverlapBox(position, new Vector2(0.5f, 0.5f), 0f, explosionLayerMask)) // 如果爆炸位置有墙壁
{
return false;
}
Vector3Int cell = wallTileMap.WorldToCell(position); // 将世界坐标转换为Tilemap的单元格坐标
TileBase tile = wallTileMap.GetTile(cell); // 获取指定单元格的Tile
if (tile != null) // 如果爆炸位置有可破坏障碍物
{
wallTileMap.SetTile(cell, null); // 清除Tile
GameObject brickWallData = Instantiate(brickWall, position, Quaternion.identity); // 实例化可破坏物体
Destroy(brickWallData, 0.5f);
return false;
} else {
//是不是最后爆炸区
if (i == explosionRange)
{
GameObject explosionEndData = Instantiate(explosionEnd, position, Quaternion.identity);
//设置爆炸效果的方向
explosionEndData.transform.eulerAngles = new Vector3(0, 0, rotate);
Destroy(explosionEndData, 0.5f);
} else {
GameObject explosionMiddleData = Instantiate(explosionMiddle, position, Quaternion.identity);
//设置爆炸效果的方向
explosionMiddleData.transform.eulerAngles = new Vector3(0, 0, rotate);
Destroy(explosionMiddleData, 0.5f);
}
return true;
}
}
效果,记得先设置和配置好墙壁的层级
八、道具生成和效果
新建破坏的墙脚本
csharp
public class BrickWall : MonoBehaviour
{
public float destructionTime = 1f;
[Range(0f, 1f)]
public float itemSpawnChance = 0.2f;//生成道具的概率
public GameObject[] spawnableItems;
private void Start()
{
Destroy(gameObject, destructionTime);
}
//销毁时按比例生成道具
private void OnDestroy()
{
if (spawnableItems.Length > 0 && Random.value < itemSpawnChance)
{
int randomIndex = Random.Range(0, spawnableItems.Length);
Instantiate(spawnableItems[randomIndex], transform.position, Quaternion.identity);
}
}
}
新增道具代码
csharp
using UnityEngine;
//道具代码
public class Prop : MonoBehaviour
{
public enum ItemType
{
ExtraBomb,
BlastRadius,
SpeedIncrease,
}
public ItemType type;
private void OnItemPickup(GameObject player)
{
switch (type)
{
case ItemType.ExtraBomb:
player.GetComponent<Bomb>().bombAmount++; // 炸弹数量加一
player.GetComponent<Bomb>().bombsRemaining++; // 剩余炸弹数量加一
break;
case ItemType.BlastRadius:
player.GetComponent<Bomb>().explosionRange++;//爆炸范围增加
break;
case ItemType.SpeedIncrease:
player.GetComponent<Player>().speed++;//移动速度增加
break;
}
Destroy(gameObject);
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player")) {
OnItemPickup(other.gameObject);
}
}
}
挂载脚本和配置好道具参数,这里为了测试方便我就把生成道具的概率先设置为1
效果
九、玩家死亡
角色添加代码
csharp
//检测碰撞 玩家死亡
private void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.layer == LayerMask.NameToLayer("Explosion")) {
animator.SetTrigger("isDeath");//播放死亡动画
//TODO:结束游戏
}
}
效果
十、简单的敌人AI
实现敌人碰壁随机往其他可移动的方向移动
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyAI : MonoBehaviour
{
private float speed = 0.05f;
private Rigidbody2D rig;
private SpriteRenderer spriteRenderer;
private Color color;
/// <summary>
/// 方向:0上 1下 2左 3右
/// </summary>
private int dirId = 0;
private Vector2 dirVector;
private float rayDistance = 0.7f;
private float x;
private float y;
private void Awake()
{
Physics2D.queriesStartInColliders = false;
spriteRenderer = GetComponent<SpriteRenderer>();
color = spriteRenderer.color;
rig = GetComponent<Rigidbody2D>();
InitDir(Random.Range(0, 4));
}
private void Update()
{
//移动
rig.MovePosition(rig.position + dirVector * speed);
}
private void InitDir(int dir)
{
Debug.Log(dir);
dirId = dir;
switch (dirId)
{
case 0:
dirVector = Vector2.up;
//控制偏移量
x = Mathf.Round(rig.position.x) < rig.position.x ? Mathf.Round(rig.position.x) + 0.5f : Mathf.Round(rig.position.x) - 0.5f;
transform.position = new Vector2(x, transform.position.y);
break;
case 1:
dirVector = Vector2.down;
x = Mathf.Round(rig.position.x) < rig.position.x ? Mathf.Round(rig.position.x) + 0.5f : Mathf.Round(rig.position.x) - 0.5f;
transform.position = new Vector2(x, transform.position.y);
break;
case 2:
dirVector = Vector2.left;
y = Mathf.Round(rig.position.y) < rig.position.y ? Mathf.Round(rig.position.y) + 0.5f : Mathf.Round(rig.position.y) - 0.5f;
transform.position = new Vector2(transform.position.x, y);
break;
case 3:
dirVector = Vector2.right;
y = Mathf.Round(rig.position.y) < rig.position.y ? Mathf.Round(rig.position.y) + 0.5f : Mathf.Round(rig.position.y) - 0.5f;
transform.position = new Vector2(transform.position.x, y);
break;
default:
break;
}
}
private void ChangeDir()
{
List<int> dirList = new List<int>();
if (Physics2D.Raycast(transform.position, Vector2.up, rayDistance).collider == null)
{
dirList.Add(0);
}
if (Physics2D.Raycast(transform.position, Vector2.down, rayDistance).collider == null)
{
dirList.Add(1);
}
if (Physics2D.Raycast(transform.position, Vector2.left, rayDistance).collider == null)
{
dirList.Add(2);
}
if (Physics2D.Raycast(transform.position, Vector2.right, rayDistance).collider == null)
{
dirList.Add(3);
}
if (dirList.Count > 0)
{
int index = Random.Range(0, dirList.Count);
InitDir(dirList[index]);
}
}
//画辅助线
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, transform.position + new Vector3(0, rayDistance, 0));
Gizmos.color = Color.blue;
Gizmos.DrawLine(transform.position, transform.position + new Vector3(0, -rayDistance, 0));
Gizmos.DrawLine(transform.position, transform.position + new Vector3(-rayDistance, 0, 0));
Gizmos.DrawLine(transform.position, transform.position + new Vector3(rayDistance, 0, 0));
}
//碰撞检测
private void OnCollisionEnter2D(Collision2D collision)
{
//碰到层级
if (collision.gameObject.layer == LayerMask.NameToLayer("Wall") || collision.gameObject.layer == LayerMask.NameToLayer("BrickWall") || collision.gameObject.layer == LayerMask.NameToLayer("Bomb"))
{
ChangeDir();
}
}
//检测敌人死亡
private void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.layer == LayerMask.NameToLayer("Explosion"))
{
Destroy(gameObject);
}
}
}
效果
十一、简单敌人AI
实现敌人随机移动
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyAI : MonoBehaviour
{
private float speed = 0.05f;
private Rigidbody2D rig;
private SpriteRenderer spriteRenderer;
private Color color;
/// <summary>
/// 方向:0上 1下 2左 3右
/// </summary>
private int dirId = 0;
private Vector2 dirVector;
private float rayDistance = 0.7f;
private float x;
private float y;
private void Awake()
{
Physics2D.queriesStartInColliders = false;
spriteRenderer = GetComponent<SpriteRenderer>();
color = spriteRenderer.color;
rig = GetComponent<Rigidbody2D>();
InitDir(Random.Range(0, 4));
}
private void Update()
{
if (GameApp.instance.enemyMove)
{
//移动
rig.MovePosition(rig.position + dirVector * speed);
}
}
private void InitDir(int dir)
{
dirId = dir;
switch (dirId)
{
case 0:
dirVector = Vector2.up;
//控制偏移量
x = Mathf.Round(rig.position.x) < rig.position.x ? Mathf.Round(rig.position.x) + 0.5f : Mathf.Round(rig.position.x) - 0.5f;
transform.position = new Vector2(x, transform.position.y);
break;
case 1:
dirVector = Vector2.down;
x = Mathf.Round(rig.position.x) < rig.position.x ? Mathf.Round(rig.position.x) + 0.5f : Mathf.Round(rig.position.x) - 0.5f;
transform.position = new Vector2(x, transform.position.y);
break;
case 2:
dirVector = Vector2.left;
y = Mathf.Round(rig.position.y) < rig.position.y ? Mathf.Round(rig.position.y) + 0.5f : Mathf.Round(rig.position.y) - 0.5f;
transform.position = new Vector2(transform.position.x, y);
break;
case 3:
dirVector = Vector2.right;
y = Mathf.Round(rig.position.y) < rig.position.y ? Mathf.Round(rig.position.y) + 0.5f : Mathf.Round(rig.position.y) - 0.5f;
transform.position = new Vector2(transform.position.x, y);
break;
default:
break;
}
}
private void ChangeDir()
{
List<int> dirList = new List<int>();
if (Physics2D.Raycast(transform.position, Vector2.up, rayDistance).collider == null)
{
dirList.Add(0);
}
if (Physics2D.Raycast(transform.position, Vector2.down, rayDistance).collider == null)
{
dirList.Add(1);
}
if (Physics2D.Raycast(transform.position, Vector2.left, rayDistance).collider == null)
{
dirList.Add(2);
}
if (Physics2D.Raycast(transform.position, Vector2.right, rayDistance).collider == null)
{
dirList.Add(3);
}
if (dirList.Count > 0)
{
int index = Random.Range(0, dirList.Count);
InitDir(dirList[index]);
}
}
//画辅助线
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, transform.position + new Vector3(0, rayDistance, 0));
Gizmos.color = Color.blue;
Gizmos.DrawLine(transform.position, transform.position + new Vector3(0, -rayDistance, 0));
Gizmos.DrawLine(transform.position, transform.position + new Vector3(-rayDistance, 0, 0));
Gizmos.DrawLine(transform.position, transform.position + new Vector3(rayDistance, 0, 0));
}
//碰撞检测
private void OnCollisionEnter2D(Collision2D collision)
{
//碰到层级
if (collision.gameObject.layer == LayerMask.NameToLayer("Wall") || collision.gameObject.layer == LayerMask.NameToLayer("BrickWall") || collision.gameObject.layer == LayerMask.NameToLayer("Bomb"))
{
ChangeDir();
}
}
//检测敌人死亡
private void OnTriggerEnter2D(Collider2D other)
{
//碰到炸弹层级,换方向
if (other.gameObject.layer == LayerMask.NameToLayer("Bomb"))
{
ChangeDir();
}
if (other.gameObject.layer == LayerMask.NameToLayer("Explosion"))
{
GameApp.instance.deathEnemyCount++;
GameApp.instance.score++;
Destroy(gameObject);
}
}
}
效果
十二、随机绘制地图
csharp
using System.Globalization;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.UIElements;
using System.Collections.Generic;
using System.Collections;
//随机生成可破坏瓦片
public class MapController : MonoBehaviour
{
public static MapController instance;
[Range(0f, 1f)]
public float itemSpawnChance = 0.2f;//生成瓦片的概率
public Tilemap map;
// 瓦片资源基类通过它可以得到瓦片资源
public TileBase tileBase;
private List<Vector3> emptyPosition;//保存所有的空位
private List<Vector3> wallPosition;//保存所有的可破坏墙的位置
public GameObject enemy;//怪物预制体
public GameObject door;//门预制体
public GameObject playerData;//主角预制体
private int mapWidth;
private int mapHeight;
private void Awake()
{
if (instance == null)
{
instance = this;
}
else
{
if (instance != this)
{
Destroy(gameObject);
}
}
DontDestroyOnLoad(gameObject);
emptyPosition = new List<Vector3>();
wallPosition = new List<Vector3>();
mapWidth = 16;
mapHeight = 8;
}
public void Init()
{
// 1.清空瓦片地图
map.ClearAllTiles();
for (int i = 0; i <= mapWidth; i++)
{
for (int j = 0; j <= mapHeight; j++)
{
//跳过砖块
if (i % 2 != 0 && j % 2 != 0)
{
continue;
}
Vector3 position = new Vector3(-7.5f + i, 3.5f - j, 0);
Vector3Int cell = map.WorldToCell(position); // 将世界坐标转换为Tilemap的单元格坐标
if (Random.value < itemSpawnChance)
{
map.SetTile(cell, tileBase); // 设置
wallPosition.Add(position);
}
else
{
emptyPosition.Add(position);
}
}
}
addPlayer();
addEnemy();
addDoor();
//清空地图的炸弹
GameObject[] res = GameObject.FindGameObjectsWithTag("Bomb");
foreach (GameObject item in res)
{
Destroy(item);
}
//清空地图的道具
res = GameObject.FindGameObjectsWithTag("Props");
foreach (GameObject item in res)
{
Destroy(item);
}
}
//生成主角
private void addPlayer()
{
GameObject res = GameObject.Find("Player(Clone)");
if (res != null)
{
Destroy(res);
}
GameObject player = Instantiate(playerData);
//生成在左下角
player.transform.position = new Vector2(-7.5f, -4.5f);
//去除相邻的map
map.SetTile(map.WorldToCell(new Vector2(-7.5f, -4.5f)), null);
map.SetTile(map.WorldToCell(new Vector2(-6.5f, -4.5f)), null);
map.SetTile(map.WorldToCell(new Vector2(-7.5f, -3.5f)), null);
emptyPosition.Remove(new Vector3(-7.5f, -4.5f, 0));
emptyPosition.Remove(new Vector3(-6.5f, -4.5f, 0));
emptyPosition.Remove(new Vector3(-7.5f, -3.5f, 0));
}
//生成怪物
private void addEnemy()
{
//清空地图上的所有敌人
GameObject[] res = GameObject.FindGameObjectsWithTag("Enemy");
foreach (GameObject item in res)
{
Destroy(item);
}
if (emptyPosition.Count > 0)
{
for (int i = 0; i < GameApp.instance.enemyCount; i++)
{
int index = Random.Range(0, emptyPosition.Count);
Instantiate(enemy, emptyPosition[index], Quaternion.identity);
}
}
}
}
效果,每次进来地图都不一样
十三、虚拟摇杆
引入虚拟摇杆,实现玩家移动,不懂的可以看我这篇文章:3种实现虚拟移动摇杆控制人物移动的方法
效果
最终效果
待续
后面还准备了一些内容,有空再补充,包括音频管理器、随机生成地图和敌人、通关门效果、主角生命值,游戏开始和结束UI界面,关卡选择界面、保存主角属性到下一关、更多的敌人(可能加入boss)、优化代码架构、完善虚拟摇杆、发布游戏到微信小游戏
源码
后面整理好我会放上来
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~