前段時間看見有一個遊戲比較火,一直沒有時間模仿着寫,最近空閒 抽出幾天時間寫寫試試吧。做出來的只能算是一個demo吧。貪吃蛇的原理應該很簡單吧,基本說一下,就是當蛇動起來的時候,蛇頭的位置被重新賦值,蛇的第二段賦原先蛇頭的位置,第三段賦原先第二段的的位置。所以原理其實很簡單。就是一個數組每幀對數組進行修改,然後新對蛇段位置重新賦值。下面先給幾張圖片
該項目只能算一個demo因爲有好多細節的東西沒有做,比如接sdk,其他的ui的一些設置都沒有,目前只有一個模式,遊戲分爲玩家蛇和一些AI蛇,所以我們需要建2個類他們分別爲PlayerSnake和AISanke,同時他們都有一些共同點,他們的移動方式是一樣的都是蛇頭先賦值 然後蛇身依次根據前面一個蛇身的位置進行賦值。並且他們都有蛇頭和蛇身之分,只是他們的控制方式不同,具體的講就是他們得到蛇頭的移動方向方式不同,一種是通過虛擬搖桿來控制蛇頭的移動方向,另外一種是通過具體的AI的控制蛇頭的移動方向。所以玩家蛇應該來說比較好寫,所以我們需建一個BaseSnake 讓玩家蛇和AI蛇都繼承BaseSnake,BaseSnake裏面就應該只有蛇的運動原理及一些基本的屬性和字段了。先貼出BaseSnake代碼
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Snake
{
public abstract class BaseSnake : MonoBehaviour,IComparable
{
public GameObject SnakeHead { set; get; }
public float SpeeRatio { set; get; }
public string NickName { set; get; }
public int SnakeIndex { set; get; }
public List<GameObject> SnakeNodes { set; get; }
public int Count { get { return SnakeNodes.Count; } }
private List<Vector3> _wayPoints;
public Vector3 MoveDir;
#region protected Variable
protected Vector3 _tempDir;
protected RaycastHit _hit;
protected int _snakeNodeCount;
protected int _snakeSkin;
protected SkinsTable.SkinItem _skinItem;
protected int _eatFoodCount;
#endregion
public virtual void OnAwake()
{
SnakeNodes=new List<GameObject>();
_wayPoints=new List<Vector3>();
SpeeRatio = 1;
NickName = GetStr.GetNickName();
}
public virtual void OnUpdate()
{
}
#region Private Method
private void AddWayPoint(Vector3 point)
{
_wayPoints.Add(point);
}
private void AddNodeObj(GameObject obj)
{
SnakeNodes.Add(obj);
}
private void AddSnakeNodes(int count, Vector3 originVector3)
{
Vector3 originPos = originVector3;
for (int i = 0; i < count; i++)
{
if (i <= count - 1)
{
GenerateBlankBody(originPos);
}
var body = new GameObject("Body", typeof(SpriteRenderer));
body.tag = "Body";
body.layer = LayerMask.NameToLayer("Body");
body.transform.position = originVector3 - 0.2f * MoveDir.normalized * (i + 1);
originPos = originVector3 - 0.2f * MoveDir.normalized * (i + 1);
if (_skinItem.Body1Sprites != null && _skinItem.Body2Sprites != null)
body.GetComponent<SpriteRenderer>().sprite = i % 2 == 0 ? _skinItem.Body1Sprites : _skinItem.Body2Sprites;
else
{
body.GetComponent<SpriteRenderer>().sprite = _skinItem.Body1Sprites;
}
body.AddComponent<CircleCollider2D>().isTrigger = true;
body.AddComponent<BodyIndex>().ParentSnakeIndex = SnakeIndex;
float value = MathTool.GetAngleByVector(new Vector2(MoveDir.x, MoveDir.y));
body.transform.eulerAngles = new Vector3(0, 0, value - 90);
body.transform.SetParent(transform);
AddNodeObj(body);
AddWayPoint(body.transform.position);
}
}
private void GenerateBlankBody(Vector3 originVector3)
{
Vector3 pos = Vector3.zero;
for (int i = 1; i < 4; i++)
{
pos = originVector3 - MoveDir.normalized * 0.2f / 4f * i;
AddWayPoint(pos);
}
}
private void MoveToTargetPos(Vector3 targetPos)
{
for (int i = _wayPoints.Count - 1; i >= 1; i--)
{
_wayPoints[i] = _wayPoints[i - 1];
}
_wayPoints[0] = new Vector3(targetPos.x, targetPos.y, 0);
for (int i = 0; i < SnakeNodes.Count; i++)
{
SnakeNodes[i].transform.position = _wayPoints[i * 4];
}
}
#endregion
#region Public Method
public virtual void IntilizedSnakePos(Vector3 pos, Vector3 direction,int snakeIndex)
{
transform.position = pos;
MoveDir = direction;
SnakeIndex = snakeIndex;
}
public virtual void IntilizedSnakeNodes(int originCount, int skinIndex,string nickName=null)
{
OnAwake();
_skinItem = SkinsTable.Instance.GetItemByIndex(skinIndex);
SnakeHead = new GameObject("SnakeHead", typeof(SpriteRenderer));
SnakeHead.tag = "Head";
SnakeHead.layer = LayerMask.NameToLayer("Body");
SnakeHead.transform.SetParent(transform);
SnakeHead.transform.localPosition = Vector3.zero;
SnakeHead.GetComponent<SpriteRenderer>().sprite = _skinItem.HeadSprite;
SnakeHead.AddComponent<Rigidbody2D>().gravityScale = 0;
var circleColider = SnakeHead.AddComponent<CircleCollider2D>();
circleColider.isTrigger = true;
circleColider.radius = 0.13f;
SnakeHead.AddComponent<BodyIndex>().ParentSnakeIndex = SnakeIndex;
float angle = MathTool.GetAngleByVector(new Vector2(MoveDir.x, MoveDir.y));
SnakeHead.transform.eulerAngles = new Vector3(0, 0, angle - 90);
Vector3 tempPos = SnakeHead.transform.position;
AddNodeObj(SnakeHead);
AddWayPoint(SnakeHead.transform.position);
AddSnakeNodes(originCount, tempPos);
}
public void EatFood(GameObject food)
{
var foodColor = food.GetComponent<FoodColor>();
_eatFoodCount += foodColor.Weight;
int count = _eatFoodCount / 4;
_eatFoodCount %= 4;
if (count != 0)
{
AddSnakeNodes(count);
}
if (foodColor.Weight <= 1)
{
foodColor.IntilizedColor();
foodColor.IntilizedWeight(1);
float x = Random.Range(-13f, 13f);
float y = Random.Range(-7f, 7f);
food.transform.localPosition = new Vector3(x, y, 0);
}
else
{
Destroy(food);
}
}
#endregion
#region Protected Method
protected void RotateHead()
{
float value = MathTool.GetAngleByVector(new Vector2(MoveDir.x, MoveDir.y));
SnakeHead.transform.eulerAngles = new Vector3(0, 0, value - 90);
Vector3 tempPos = SnakeHead.transform.position;
for (int i = 1; i < SnakeNodes.Count; i++)
{
Vector3 dir = (tempPos - SnakeNodes[i].transform.position);
var angle = MathTool.GetAngleByVector(new Vector2(dir.x, dir.y));
SnakeNodes[i].transform.eulerAngles = new Vector3(0, 0, angle);
}
}
protected void MoveSnake()
{
for (int i = 0; i < SpeeRatio; i++)
{
Vector3 endPos = Vector3.zero;
if (SnakeNodes[0] != null)
endPos = SnakeNodes[0].transform.position + 0.2f / 4f * MoveDir.normalized;
MoveToTargetPos(endPos);
}
}
protected void AddSnakeNodes(int count)
{
Vector3 tempPos = SnakeNodes[SnakeNodes.Count - 1].transform.position;
AddSnakeNodes(count, tempPos);
}
#endregion
public int CompareTo(object obj)
{
var snake = obj as BaseSnake;
if (this.Count > snake.Count)
return 1;
if (this.Count == snake.Count)
return 0;
if (this.Count < snake.Count)
return -1;
return -1;
}
}
}
總共代碼就200多行,這些私有方法就不用看了,只需看看Public方法和一些Protect方法了,首先讓2個有關生命週期的方法OnAwake和OnUpdate方法虛化,目的是讓他的2個子類去重寫它,另外有2個初始化的方法也被虛化了,其實IntilizedSnakePos這個方法沒有虛化的必要性,因爲在他們的子類中都沒有重寫,這樣寫的原因只是爲了看起來比較好看一些。另外還有一個EatFood,這個方法玩家蛇和AI蛇都應該具有的行爲。至於RotateHead,MoveSnake和AddSnakeNodes都是一些關於蛇移動處理和蛇身添加處理的方法,RotateHead主要旋轉蛇頭並且旋轉蛇身的方向。MoveSnake主要是移動整條蛇。最後一個方法純粹就是給蛇添加蛇身。旋轉蛇頭涉及一個簡單的數學知識。這裏可以簡單的講講,就是我們再二維座標中隨便給出一個向量,我們就知道它的方向,例如(1,0)我們知道它對應得旋轉量爲(0,0,0),(0,1)對應得旋轉量爲(0,0,90),所以我們的根據具體的向量來計算出蛇頭的朝向問題。首先我們需要讓蛇頭的移動向量和(1,0)做點乘,然後通過點乘求得一個角度,這個角度在0到180度之間,因爲點乘的求得只是夾角。例如向量(0,1)我們求得夾角爲90,那麼它對應的歐拉角可能是(0,0,90)和(0,0,-90)了,所以我們得從其中選取正確的一個值,這是我們再做一下簡單的叉乘,用叉乘求出來一個向量,判斷向量實在朝上還是朝下,如果是朝上說明正90,相反就是-90了,具體方法如下
public static float GetAngleByVector(Vector2 dir)
{
Vector2 relativeVector = new Vector2(1, 0);
float value = Vector2.Dot(dir.normalized, relativeVector);
float angle = Mathf.Acos(value) * Mathf.Rad2Deg;
float value1 = relativeVector.x*dir.y - relativeVector.y*dir.x;
angle *= value1 > 0 ? 1 : -1;
return angle;
}
遊戲雖然是很簡單,但這樣說的目的只是在怎麼去組織自己的代碼。下節繼續把這個demo說完。