Siki_Unity_1-9_Unity2D游戏开发_Roguelike拾荒者
Posted on 2017-12-21 18:58 FudgeBear 阅读(2454) 评论(0) 编辑 收藏 举报Unity 1-9 Unity2D游戏开发 Roguelike拾荒者
任务1:游戏介绍
Food:相当于血量:每走一步下降1,吃东西可以回复(果子10药水20),被怪物攻击会减少
中间的障碍物可以打破,人走两步僵尸走一步;走到Exit进入下一关
最外圈的过道会保证是空的,其他的随机生成(--确保主角能够到达出口)
任务2:创建工程、素材
pan.baidu.com/s/1kTYS8ez
Unity5.2.1
2D Project -- Roguelike
导入Assets.unitypackage
Sprites->所有切好的图片 -- 主角/地形/出口/围墙/僵尸等
任务3:创建游戏主角
Sprites中找到6个主角Scavengers的sprite图片,拖入Hierachy中
会自动在游戏物体Scavengers_SpritesSheet_0中创建一个动画Animator,并将Animation
和AnimationController保存在Sprites文件夹中,重命名控制器为Player/ 动画为PlayerIdle
并创建文件夹分类Assets->Animations->Animation/AnimatorController
此时,运行游戏,会自动播放Idle动画
双击AnimatorController打开动画编辑器,修改速度为0.5
创建攻击动画:
将两个攻击Sprites拖入Player物体,会自动在Player中创建一个新的动画PlayerAttack
创建受攻击动画:相似 -- PlayerUnderAttack
创建主角的Prefab
任务4:创建敌人
与创建主角相似
创建完Enemy1,创建Enemy1的Prefab
因为Enemy2的动画控制器AnimatorController与Enemy1相同
在Project视窗中右键Create->Animator Override Controller
将Enemy1的Animator赋值给新建的Animator -- 表示两个Enemy共用一个状态机
将Hierarchy中的Enemy1->GameObject->Break prefab instance,重命名为Enemy2
将Enemy2 Animator赋值给Enemy2
创建Enemy2的两个动画,将动画拖入Enemy2 Animator赋值即可
相当于:Enemy1和Enemy2共用了Enemy1的状态机(Animator),但是使用了不同动画
创建Enemy2的Prefab
任务5:创建地板/ 围墙/ 食物
创建地板:
把8种地板和出口分别拖入,并重命名Floor1~8/ Exit
把8中障碍物拖入,重命名为Wall1~8
做成Prefab
创建围墙:三种围墙,重命名为OutWall1~3并做成Prefab
创建食物:拖入Soda和Food,做成Prefab
任务6:生成地图
创建空物体GameManager
创建MapManager.cs
// 地图左下角设置为(0, 0)
public GameObject[] outWallArray/ floorArray;
// 创建根物体
private Transform mapHolder;
mapHolder = new GameObject("Map").transform;
// 初始化围墙
for x for y(...) {
GameObject cell;
if (x==0 || y==0 || x==cols-1 || y==rows-1) {
int index = Random.Range(0, outWallArray.Length);
cell = GameObject.Instantiate
(outWallArray[index], ...(x, y, 0), Quaternion.identity) as GameObject;
// as GameObject -- .Instantiate()生成的为Object类型,强制转化为GameObject
// 在(x,y,0)处生成一个旋转为0的围墙
} else ...
cell.transform.SetParent(mapHolder);
}
// 初始化地板
else {
int index = Random.Range(0, floorArray.Length);
cell = GameObject.Instantiate
(floorArray[index], ...(x,y,0), Quaternion.identity) as GameObject;
}
此时生成的地图并不位于相机正中心
-- 地图长10宽10 -- (每一格长宽为1,从(0,0)开始,即画面从(-0.5, -0.5开始))
--> Camera位置:(4.5, 4.5, 0),颜色改为黑色
private void InitMap() { for (int x = 0; x < cols; x++) { for (int y = 0; y < cols; y++) { GameObject cell; if (x == 0 || x == cols - 1 || y == 0 || y == rows - 1) { int index = Random.Range(0, outWallArray.Length); cell = GameObject.Instantiate (outWallArray[index], new Vector3(x, y, 0), Quaternion.identity); } else { int index = Random.Range(0, floorArray.Length); cell = GameObject.Instantiate (floorArray[index], new Vector3(x, y, 0), Quaternion.identity); } cell.transform.SetParent(mapHolder); } } }
任务7:控制障碍物的生成
在InitMap()中
private List<Vector2> positionList = new List<Vector2>(); // 用于取得中间部分的格子位置
positionList.Clear();
for(int x/y = 0 + 2; x/y < cols/rows - 2; x/y++) { // 围墙一列,空道一列
positionList.Add(new Vector2(x, y));
}
// 从上面的positionList中随机取得格子放入障碍物/食物/敌人
// 创建障碍物
public GameObject[] wallArray;
public int min/maxCountWall = 2/8;
int wallCount = Random.Range(minCountWall, maxCountWall + 1);
for(int i = 0~wallCount) {
// 随机取得格子位置
int positionIndex = Random.Range(0, positionList.Count); // 随机取得index
Vector2 pos = positionList[positionIndex]; // 得到位置信息
positionList.RemoveAt[positionIndex]; // 移除该位置 -- 保证一个格子只能有一个东西
// 随机取得障碍物
int wallIndex = Random.Range(0, wallArray.Length);
GameObject cell = GameObject(wallArray[...], pos, ...) as GameObject;
cell.transform.SetParent(mapHolder);
}
避免地板将障碍物覆盖,设置Layer:Background/ Items/ Roles
positionList = new List<Vector2>(); positionList.Clear(); for(int x = 0+2; x < cols - 2; x++) { for(int y = 0 + 2; y < rows - 2; y++) { positionList.Add(new Vector2(x, y)); } } int wallCount = Random.Range(minCountWall, maxCountWall + 1); for(int i = 0; i < wallCount; i++) { int positionIndex = Random.Range(0, positionList.Count); Vector2 pos = positionList[positionIndex]; positionList.RemoveAt(positionIndex); int wallIndex = Random.Range(0, wallArray.Length); GameObject cell = GameObject.Instantiate (wallArray[wallIndex], pos, Quaternion.identity) as GameObject; cell.transform.SetParent(mapHolder); }
任务8&9:敌人和食物的随机生成 & 代码优化
食物和敌人的数量与关卡有关 -- 数量成正比
创建GameManager.cs -- 控制游戏关卡
public int level = 1;
在MapManager.cs中
// 获取GameManager
private GameManager gameManager;
gameManager = GetComponent<GameManager>();
创建食物 -- 数量2~level*2
int foodCount = Random.Range(2, gameManager.level * 2 + 1);
// 取得随机位置 -- 重复代码写成Vector2 RandomPosition()
Vector2 pos = RandomPosition();
// 随机取得物体 -- 重复代码写成GameObject RandomPrefab(GameObject[] prefabs);
GameObject foodPrefab= Instantiate(RandomPrefab(foodArray)) as GameObject;
foodPrefab.transform.setParent(mapHolder);
// get a random available position private Vector2 RandomPosition() { int positionIndex = Random.Range(0, positionList.Count); Vector2 pos = positionList[positionIndex]; positionList.RemoveAt(positionIndex); return pos; } // get a random gameobject private GameObject RandomPrefab(GameObject[] prefabs) { int index = Random.Range(0, prefabs.Length); return prefabs[index]; }
创建敌人 -- 数量为level / 2
int enemyCount = gameManager.level/2;
for(0~enemyCount) {
Vector2 pos = ...;
GameObject enemyPrefab = GameObject.Instantiate(...) as GameObject;
enemyPrefab.transform.setParent(mapHolder);
}
创建出口 -- 位置固定在右上方
GameObject exit =
(Instantiate(exitPrefab, new Vector2(cols-2, rows-2), Quaternion.identity) as GameObject;
exti.transform.SetParent(mapHolder);
-- 代码优化
上面的创建代码是可重用的 -- 写成method
private void InstantiateItems(int count, GameObject[] itemArray) { for(int i = 0; i < count; i++) { GameObject item = GameObject.Instantiate(RandomPrefab (itemArray), RandomPosition(), Quaternion.identity) as GameObject; item.transform.SetParent(mapHolder); } }
任务10:完善主角和敌人的动画状态机
Player的动画:PlayerIdle/ PlayerUnderAttack/ PlayerAttack
PlayerIdle<-->PlayerUnderAttack
PlayerIdle<-->PlayerAttack
在Animator中添加触发器Trigger分别称为Damage和Attack
Idle->Attack/UnderAttack:
将Has Exit Time取消勾选 -- 在Idle动画的任何时候都可以随时切换到另一个动画
Transition Duration = 0 -- 因为这里的动画是帧动画,因此可以进行瞬时切换成其他动画
Conditions 切换方式:添加Attack/Damage Trigger
拖动左边的箭头可以手动播放动画;拖动右边的两个箭头可以手动控制何时切换
Attack/UnderAttack->Idle:
勾选Has Exit Time即可 -- 播放完Attack/UnderAttack后自动切换到Idle动画
Transition Duration = 0 -- 瞬时间切换
Exit Time = 1 -- 退出时间(多久进行切换)
Enemy的动画:EnemyIdle/ EnemyAttack
Idle->Attack:
添加触发器Attack
Has Exit Time uncheck
Transition Duration = 0
Attack->Idle:
Has Exit Time check
Exit Time = 1
Transistion Duration = 0
检测动画:
运行游戏;选中Player/ Enemy;在Animator视窗中查看状态机
点击Trigger右边的小圆点,即为触发该Trigger
任务11:控制主角的运动
为Player添加刚体,用于控制移动,勾选Is Kinematic
为Player添加BoxCollider2D,用于检测碰撞(大小不能设置为(1,1),0.9就好)
为Player添加Player.cs来控制移动
private int posx/posy = 1; // 当前位置
private int Vector2 targetPos = new Vector2(1,1); // 目标位置
float h/v = Input.GetAxisRaw("Horizontal"/"Vertical"); // GetAxisRaw()的返回值为-1/0/1
targetPos += new Vector2(h,v); // 按键后,目标位置发生改变
private Rigidbody2D rigidbody = GetComponent<Rigidbody2D>(); // 得到刚体,用于移动
rigidbody.MovePosition(Vector2.Lerp(transform.position, targetPos, smoothing*Time.deltaTime));
// MovePosition(目标位置):向目标位置移动
// Lerp(起点,终点,速度);这里设smoothing=1
发现此时按下按钮会移动很长的距离 -- Update中不停调用targetPos += Vector2();
需要设置一个休息时间:
public float restTime = 0.5f;
public float restTimer = 0.5f; // 计时器
在Update()中
restTimer += Time.deltaTime; // 每次增加时间间隔
if(restTimer <= restTime) { // 还在休息间隔中
return; // 不进行其他操作
}
当有按键按下时:
if(h!=0 || v!=0) {
targetPos += ...; // 更新目标位置
rigidbody.MovePosition(...); // 移动
restTimer = 0; // 重置计时器
}
按下按键时,只能移动一点点距离
将rigidbody.MovePosition()代码移出if条件
因为每一帧都需要调用而不是只有按键的时候调用
发现可以一次同时在水平和竖直移动 -- 和游戏规则有悖
if(h!=0) v=0; // 优先控制竖直方向
void Update () { rigidbody.MovePosition(Vector2.Lerp(transform.position, targetPos, smooth * Time.deltaTime)); if (restTimer < restTime) { restTimer += Time.deltaTime; return; } float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); if (h != 0) { v = 0; } if (h != 0 || v != 0) { // 有按键输入时 targetPos += new Vector2(h, v); restTimer = 0; // 计时器归零 } }
任务12:控制主角对墙体的攻击
给outWall添加BoxCollider2D,scale=0.9;添加tag = OutWall
给wall添加BoxCollider2D,scale=0.9;添加tag = Wall
在每次按键的时候,进行碰撞检测:
RaycastHit2D hit = Physics2D.Linecast(targetPos, targetPos + new Vector2(h, v));
// Physics2D.Linecast(起点,终点) 从起点向终点发射射线做检测,返回RaycastHit2D
if(hit.transform == null) { // 没有碰撞,可以行走
} else { // 有碰撞
switch(hit.collider.tag) {
case "OutWall": // 不做处理
case "Wall": // 进行攻击
// 向墙发送攻击的通知
// 在Wall里添加Wall.cs来处理行为 Wall.TakeDamage();
hit.collider.SendMessage("TakeDamage");
public int hp = 2; public Sprite damageSprite; // 受损后的墙体图片 public void TakeDamage() { hp -= 1; GetComponent<SpriteRenderer>().sprite = damageSprite; if(hp<=0) Destroy(this.gameObject); }
禁用自身的collider,因为可能射线会碰到自身的collider:
private BoxCollider2D collider = GetComponent<...>();
collider.enabled = false;
... // 射线检测
collider.enabled = true; // 检测后再启用
无论是移动还是攻击,都需要休息 -- 只要按下了按键,restTimer = 0;
攻击时,播放PlayerAttack动画
Player.cs
private Animator animator = GetComponent<Animator>();
animator.SetTrigger("Attack"); // 触发触发器
rigidbody.MovePosition( Vector2.Lerp(transform.position, targetPos, smooth * Time.deltaTime)); if (restTimer < restTime) { restTimer += Time.deltaTime; return; } float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); if (h != 0) {v = 0;} if (h != 0 || v != 0) { // 有按键输入时 collider.enabled = false; // 禁用自身collider // 碰撞检测 RaycastHit2D hit = Physics2D.Linecast(targetPos, targetPos + new Vector2(h, v)); if (hit.transform == null) { // 没有检测到碰撞 targetPos += new Vector2(h, v); } else { // 检测到碰撞 switch (hit.collider.tag) { case "OutWall": break; case "Wall": hit.collider.SendMessage("TakeDamage"); animator.SetTrigger("Attack"); break; } } restTimer = 0; // 计时器归零 collider.enabled = true; // 检测完碰撞后 开启自身collider }
任务13:控制主角吃食物
对食物进行碰撞检测:
对食物添加BoxCollider2D;设置为Is Trigger,scale=0.9,tag=food
存储当前食物 GameManager.public int food = 100;
将GameManager设置为单例模式
private static GameManager _instance; public static GameManager Instance { get{ return _instance; } } private void Awake() { _instance = this; }
增加/减少食物的method:public void Add/ReduceFood(int count)
food +=/-= count;
case "Food":
// 用if判断hit.collider.name是Food(Clone)还是Soda(Clone)
GameManager.Instance.AddFood(10 | 20);
// 同时,需要移动Player位置 && 销毁食物
targetPos += new Vector2(h, v);
Destroy(hit.collider.gameObject);
case "Food": targetPos += new Vector2(h, v); Destroy(hit.collider.gameObject); if (hit.collider.name == "Food(Clone)") { GameManager.Instance.AddFood(10); } else if(hit.collider.name == "Soda(Clone)") { GameManager.Instance.AddFood(20); } break;
任务14&15:控制敌人的移动
当Player移动两步时,敌人移动一步
给Enemy添加BoxCollider2D进行碰撞检测,size=0.9,
给Enemy添加Rigidbody2D进行移动,勾选Is Kinematic:很重要
如果没有勾选,会导致后面Enemy被Player推走
给Enemy添加tag=Enemy
case "Enemy": // 说明所走的路径不通,判断失误,浪费一步
break;
给Enemy添加Enemy.cs
Enemy是被动移动 -- 提供移动方法,供其他对象调用
public void Move() {
// 判断Player所在位置的方向
// Transform player = GameObject.FindGameObjectWithTag(...)
Vector2 offset = player.position - transform.position;
if (offset.magnitude < 1.9f) { // 距离小于一格
// 攻击
} else { // 追 -- 哪个轴偏移大,就往哪里追
if (Mathf.Abs(offset.x) > Mathf.Abs(offset.y)) { // x轴移动
if(offset.x<0) x=-1; else x=1;
} else { // y轴移动
if(offset.y<0) y=-1; else y=1;
}
targetPos += new Vector(x, y);
}
在GameManager.cs中统一管理游戏进程:
public List<Enemy> enemyList = new List...;
在Enemy.cs中:
GameManager.Instance.enemyList.Add(this);
在GameManager.cs中控制Enemy是否行走:
bool sleepStep = true; // 是否为休息状态
OnPlayerMove() {
if(sleepStep) sleepStep = false;
else { foreach enemy in enemyList { enemy.Move();
sleepStep = true; }
}
在Player.cs中
有按键按下的时候调用OnPlayerMove()
GameManager.Instance.OnPlayerMove();
此时,Enemy会随着主角移动而移动,但是Enmey没有碰撞检测
设置目标位置targetPos之前先做检测
// 更新位置之前,做碰撞检测 collider.enabled = false; // 否则会碰撞到自己的collider RaycastHit2D hit = Physics2D.Linecast(targetPos, targetPos+new Vector2(x,y)); collider.enabled = true; if (hit.collider == null) { // 没有碰撞 targetPos += new Vector2(x, y); } else { // 有碰撞 switch (hit.collider.tag) { case "Wall": case "OutWall": // 不可能出现外墙的情况 break; case "Food": Destroy(hit.collider.gameObject); targetPos += new Vector2(x, y); break; } }
任务16:控制敌人的攻击
private Animator animator = GetComponent<...>();
animator.SetTrigger("Attack");
player.SendMessage("TakeDamage", lossFood); // 不同敌人的伤害不同
public void TakeDamage(int lossFood) {
GameManager.Instance.ReduceFood(lossFood);
animator.SetTrigger("UnderAttack");
}
任务17:控制游戏食物数量UI的显示
在Player每走一步就消耗一定量食物:
GameManager.Instance.ReduceFood(1);
在屏幕上显示食物:
右键创建UI->Text,重命名FoodText
字体剧中,字号25,字体为Font->PressStart2P-Regular,内容:Food: 100
放置在屏幕下方,Anchor Presets设置为bottom-center
在GameManager.cs中控制UI的更新
private Text foodText;
在void Awake()中 加入 InitGame();
void InitGame() {
foodText = GameObject.Find("FoodText").GetComponent...;
UpdateFoodText(0);
}
private void UpdateFoodText(int foodChange) { if (foodChange == 0) { foodText.text = "Food: " + food; } else if (foodChange > 0) { foodText.text = "+" + foodChange + " Food: " + food; } else { foodText.text = foodChange + " Food: " + food; } }
在ReduceFood()/ AddFood(){} 中加上 UpdateFoodText(count/-count);
任务18:控制游戏的失败
游戏失败:Food <= 0;
Player每走完一步:检测food的数量
-- 我的优化:在GameManager中的ReduceFood()中检测 -- 没有必要每一步都检测
if(food<=0){
GameObject.FindGameObjectWithTag("Player").SetActive(false);
// 显示游戏失败 -- UI->Text,居中,白色,字号,字体,GameOver等等
gameOverText.enabled = true;
}
任务19&20:游戏关卡的胜利判断 && 下一关卡的加载
判断Player是否到达了Exit的位置(8,8)
每次Player移动,判断是否到达终点
Player每次移动,都会调用GameManager中的OnPlayerMove()
在OnPlayerMove()中加入
// 需要得到player的targetPos,与destinationPos比较
将targetPos改为[HideInInspector]public targetPos;
Player player = GameObject.Find...Tag(...).GetComponent...;
// 还需得到Exit的位置
MapManager mapManager = GetComponent...;
if(player.targetPos.x == mapManager.cols-2 && ...y==rows-2) {
private bool missionCompleted = true;
// 加载下一关卡
Application.LoadLevel(Application.loadedLevel); // 重新加载本关卡 -- 已弃用
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
// 需要记住某些值 -- 使用系统函数OnLevelWasLoaded()
}
运行 -- 发现可以重置关卡了,但是所有数据会重置
GameManager实例不能销毁 -- 否则数据会重置
在GameManager.cs的Awake()中:
DontDestroyOnLoad(gameObject);
运行 -- 发现原来的GameManager没有销毁,但是新建了另一个GameManager
不将GameManager放在场景中
-- 将GameManager做成Prefab
-- 删除场景中的GameManager
在MainCamera中添加GameLoader.cs
// 实例化GameManager public GameObject gameManager; void Awake() { if(GameManager.Instance == null) { GameObject.Instantiate(gameManager); } }
任务21:控制天数UI的显示
当前天数的显示
UI->Image->DayImage; alt+上下左右居中;颜色设置为黑色
在DayImage中创建一个Text叫DayText;居中偏上;字号32;字体自定义;颜色白色
什么时候显示呢? -- 初始化UI的时候需要显示
GameManage.InitGame()中:
private Image dayImage = GameObject.Find("DayImage").GetComponent<Image>();
private Text dayText ...;
dayText.text = "Day " + level;
显示完天数,需要隐藏
void HideDayImage() { dayImage.gameObject.SetActive(false); }
在InitGame()最后调用 Invoke("HideDayImage", 1); 即可 -- 过1s后调用
任务22:添加音效
GameManager.cs