作为红白机时代的经典游戏,90坦克绝对算是年代表作品之一,今天我想用读取配置表的方式复刻一下这儿时的经典。
1.场景建设
首先将准备好的场景素材导入工程,将其中的砖墙的Tag设置为Wall,草地Tag设置为Wall1,水墙设置为Wall2,铁墙Wall3,并在每个物体身上添加Box Collider2D组件,设置三个空节点,用来挂载敌人和玩家和子弹。

上图中白色图片为敌人生成点。
2.数据配置表
作为可拓展、可配置的坦克大战当然离不开配置表的设置,你可以在配置表里面添加一切你想拥有的角色属性或道具属性等等。
玩家表分析设计:
1.需要一个唯一ID来区别表中的数据,方便索引;
2.需要等级来绑定显示和子弹类型;
3.需要图片名称,用来加载图片用;
4.需要子弹类型,用来显示不同的子弹效果,及能打穿铁墙和不能;
敌人表分析设计:
1.需要一个唯一ID来区别表中的数据,方便索引;
2.需要等级来绑定显示和子弹类型;
3.需要图片名称,用来加载图片;
4.需要子弹类型,用来显示不同的子弹效果,及能打穿铁墙和不能;
道具分析:
1.添加ID来,方便索引;
2.添加图片名称,用来显示道具外形;
(勤劳的我,会告诉你就做了一个道具么?)
我的配置表如下:
1.玩家配置表

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ID为1的玩家,等级为1,外观图片是Character1,子弹种类为1</figcaption>
2.敌人配置表

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ID为1的敌人,等级为1,外观图片是Monster1,子弹种类为1</figcaption>
3.道具配置表

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ID为1的道具,外观图片是Star</figcaption>
做好表格之后需要将表格放入Unity中读取,这中间我用了一个小工具将.xlsx格式的文件转化为TXT文本,并放在了Assets根目下,然后用Json插件读取表格信息并写入字典,当游戏中需要对应数据时new一个新的物体出来附上对应的值,从而获取动态数据。代码方式如下:
//构造静态数据类,他们的组成与表格对应
public class StaticMonsterData
{
public readonly string id;
public readonly string level;
public readonly string pictrue;
public readonly string buttle;
public float Value(string m)
{
float intA;
float .TryParse(m, out intA);
return intA;
}
}
public class StaticPropData
{
public readonly string ID;
public readonly string Pictrue;
}
public class StaticCharacterData
{
public readonly string id;
public readonly string level;
public readonly string pictrue;
public readonly string buttle;
public float Value(string m)
{
float intA;
float .TryParse(m, out intA);
return intA;
}
}
public class StaticData : MonoBehaviour {
public static StaticData Instance;
public Dictionary<string, StaticMonsterData> monsterDictionary;
public Dictionary<string, StaticCharacterData> characterDictionary;
public Dictionary<string, StaticPropData> propdictionary;
void Awake()
{
Instance = this;
Init();
LoadSence();
}
// 初始化,将表格数据写如字典
void Init()
{
monsterDictionary = Load<Dictionary<string, StaticMonsterData>>("Monster");
characterDictionary = Load<Dictionary<string, StaticCharacterData>>("Character");
propdictionary = Load<Dictionary<string, StaticPropData>>("Prop");
}
// 载入场景,并挂载GameMode脚本
void LoadSence()
{
var temp = Resources.Load<GameObject>("Sence").gameObject;
var sence = Instantiate(temp);
sence.name = temp.name;
sence.AddComponent<GameMode>();
}
//获取TXT文本并赋值给对应的类,并返回T的类型值;
public static T Load<T>(string name)
{
string rName = name;
string readFilePath= "Assets/" + rName + ".txt";
string str;
T data = default(T);
if (File.Exists(readFilePath))
{
StreamReader textData = File.OpenText(readFilePath);
str = textData.ReadLine();
textData.Close();
textData.Dispose();
data = JsonMapper.ToObject<T>(str);
}
return data;
}
}
动态数据的获取:
//建立角色类
public class RoleData
{
public string id;
public float level;
public string picture;
public float speed;
public float bullet;
}
//玩家类继承角色类
public class CharacterData : RoleData
{
///建立构造函数,获取静态数据
public static CharacterData GetCharacterData(StaticCharacterData data)
{
CharacterData characterData = new CharacterData();
characterData.id = data.id;
characterData.level = data.Value(data.level);
characterData.picture = data.pictrue;
characterData.bullet = data.Value(data.buttle);
return characterData;
}
}
public class MonsterData : RoleData
{
public static MonsterData GetMonsterData(StaticMonsterData data)
{
MonsterData monsterData =new MonsterData();
monsterData.id = data.id;
monsterData.level = data.Value(data.level);
monsterData.picture = data.pictrue;
monsterData.bullet = data.Value(data.buttle);
return monsterData;
}
}
public class PropData
{
public string id;
public string pictrue;
public static PropData GetPubicPropData(StaticPropData data)
{
PropData propData = new PropData();
propData.id = data.ID;
propData.pictrue = data.Pictrue;
return propData;
}
}
3.GameMode
当数据获取完毕,场景载入开始时,需要加载玩家角色,敌人角色,道具信息等,并将他们对应的表信息转换成能随时调用的动态信息赋值给他们。
代码如下:
void Start()
{
home = gameObject.transform.Find("CharacterHome").gameObject;
home1 = gameObject.transform.Find("MonsterHome/Home1").gameObject;
home2 =gameObject .transform .Find("MonsterHome/Home2").gameObject;
home3 = gameObject.transform.Find("MonsterHome/Home3").gameObject;
home4 = gameObject.transform.Find("MonsterHome/Home4").gameObject;
animator1 = home1.GetComponent<Animator>();
animator2 = home2.GetComponent<Animator>();
animator3 = home3.GetComponent<Animator>();
animator4 = home4.GetComponent<Animator>();
Init();
}
void CreatMonster(string ID, GameObject Object)
{
var Data = StaticData.Instance.monsterDictionary[ID];//获取字典中相应key值的静态数据
var monsterData = MonsterData.GetMonsterData(Data);//将静态数据转换成动态数据
var temp = Resources.Load<GameObject>("Monster");
var monster = Instantiate(temp, Object.transform);
monster.AddComponent<AiMonster>().data = monsterData;//将动态数据赋值给角色。
}
//获取KEY值为1的静态数据,转化赋值给生成的玩家。
void CreatCharacter()
{
var Data = StaticData.Instance.characterDictionary["1"];
var characterData = CharacterData.GetCharacterData(Data);
var temp = Resources.Load<GameObject>("Character");
var character = Instantiate(temp, home.transform);
character.AddComponent<Character>().data = characterData;
}
//随机地点生成道具。并赋值
void CreatProp()
{
var data = StaticData.Instance.propdictionary["1"];
var propData = PropData.GetPubicPropData(data);
var temp = Resources.Load<GameObject>("Prop");
var prop = Instantiate(temp,gameObject.transform);
prop.transform.position += new Vector3(Random.Range(-480, 200),Random.Range(350,-350));
prop.AddComponent<Prop>().data = propData;
}
void Init()
{
animator1.Play("Start", 0);
animator2.Play("Start", 0);
animator3.Play("Start", 0);
animator4.Play("Start", 0);
//2s后创建敌人
StartCoroutine(OnDelay(2.0f, () =>
{
CreatMonster("1", home1);
CreatMonster("2", home2);
CreatMonster("3", home3);
CreatMonster("4", home4);
}));
CreatCharacter();
}
int times = 0;
void Update ()
{
if (isGameover == false)
{
times++;
CreatProp();
if (times == 3)
{
isGameover = true;
}
}
}
IEnumerator OnDelay(float time, UnityEngine.Events.UnityAction rFunc)
{
yield return new WaitForSeconds(time);
rFunc();
yield return null;
}
}
效果显示:

4.玩家
简单的搭建好玩家模型,设计玩家行为:
- 玩家可以上、下、左、右四个方向移动;
- 每次移动距离为场景中一个墙体的1/2宽度距离;
- 玩家无法穿越障碍;
- 玩家可以发射子弹;
- 玩家可以获得道具;
- 玩家获取道具升级之后可以改变外形和字典效果;
- 玩家可以死亡;
代码如下:
void Start()
{
//获取玩家模型组件
muzzle = gameObject.transform.Find("MuzzlePiont").gameObject;
rayPiont1 = gameObject.transform.Find("RayPiont1").gameObject;
rayPiont2 = gameObject.transform.Find("RayPiont2").gameObject;
bulletPiont = GameObject.Find("BulletPiont");
animator = gameObject.GetComponent<Animator>();
bron = gameObject.transform.Find("Image").GetComponent<Image>();
//调用函数
Init(data);
OnPlayAnimation();
}
// 播放出生光圈,并在3S后隐藏该组件
private void OnPlayAnimation()
{
animator.Play("Bron", 0);
StartCoroutine(OnDelay(3, () =>
{
bron.gameObject.SetActive(false);
}));
}
//初始化玩家,画出当前数据下玩家的外形
void Init(CharacterData data)
{
draw = gameObject.transform.Find("Draw").GetComponent<Image>();
var image_Show = Resources.Load<Sprite>("Image/" + data.picture);
draw.sprite = image_Show;
}
void Update()
{
//判断当前是正在移动
if (OnMove == false)
{
if (Input.GetKey(KeyCode.D))
{
OnMove = true;
MoveVector = new Vector3(1, 0, 0);
endPosition = transform.position + MoveVector * 25;// 设定每次移动25为1/2墙宽
}
if (Input.GetKey(KeyCode.S))
{
OnMove = true;
MoveVector = new Vector3(0, -1, 0);
endPosition = transform.position + MoveVector * 25;
}
if (Input.GetKey(KeyCode.A))
{
OnMove = true;
MoveVector = new Vector3(-1, 0, 0);
endPosition = transform.position + MoveVector * 25;
}
if (Input.GetKey(KeyCode.W))
{
OnMove = true;
MoveVector = new Vector3(0, 1, 0);
endPosition = transform.position + MoveVector * 25;
}
}
if (OnMove)
{
Move();
}
Fire();
}
void Move()
{
//当初始位置和结束位置相等时,可以接受重新输入移动方向。
if ((endPosition - transform.position).magnitude ==0)
{
OnMove = false;
return;
}
//转向
Trun(MoveVector);
//向移动方向打出两条射线
RaycastHit2D hit1 = Physics2D.Raycast(rayPiont1.transform.position, MoveVector, 0.1f);
RaycastHit2D hit2 = Physics2D.Raycast(rayPiont2.transform.position, MoveVector, 0.1f);
// 射线 碰撞判定
if (hit1.collider != null || hit2.collider != null)
{
if (hit1.collider == null)
{
if (hit2.collider.tag == "Prop")
{
if (data.id == "1")
{
var Data = StaticData.Instance.characterDictionary["2"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else if (data.id == "2")
{
var Data = StaticData.Instance.characterDictionary["3"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else if (data.id == "3")
{
var Data = StaticData.Instance.characterDictionary["4"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else
{
return;
}
}
if (hit2.collider.tag == "Wall1")
{
transform.position += MoveVector;
}
else
{
OnMove = false;
}
}
else if (hit2.collider == null)
{
if (hit1.collider.tag == "Prop")
{
Destroy(hit1.collider.gameObject);
if (data.id == "1")
{
var Data = StaticData.Instance.characterDictionary["2"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else if (data.id == "2")
{
var Data = StaticData.Instance.characterDictionary["3"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else if (data.id == "3")
{
var Data = StaticData.Instance.characterDictionary["4"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
}
if (hit1.collider.tag == "Wall1")
{
transform.position += MoveVector;
}
else
{
OnMove = false;
}
}
else
{
if (hit1.collider.tag == "Prop")
{
if (data.id == "1")
{
var Data = StaticData.Instance.characterDictionary["2"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else if (data.id == "2")
{
var Data = StaticData.Instance.characterDictionary["3"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else if (data.id == "3")
{
var Data = StaticData.Instance.characterDictionary["4"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
}
else if (hit2.collider.tag == "Prop")
{
if (data.id == "1")
{
var Data = StaticData.Instance.characterDictionary["2"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else if (data.id == "2")
{
var Data = StaticData.Instance.characterDictionary["3"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else if (data.id == "3")
{
var Data = StaticData.Instance.characterDictionary["4"];
var characterData = CharacterData.GetCharacterData(Data);
data = characterData;
Init(data);
}
else
{
return;
}
}
if (hit1.collider.tag == "Wall1" && hit2.collider.tag == "Wall1")
{
transform.position += MoveVector;
}
else
{
OnMove = false;
}
}
}
else
{
transform.position += MoveVector;
}
}
void Trun(Vector3 moveVector)
{
var angel = Vector3.Angle(transform.right, moveVector);//计算当前方向和下一个方向的角度
var normal = Vector3.Cross(transform.right, moveVector);//计算当前向量和下一个移动方向的叉乘得出它们所在平面的垂直向量
angel *= Mathf.Sign(Vector3.Dot(transform.forward, normal));//技算当前向量和下个移动方向向量的点积,并返回正负号赋值给角度
var vector = transform.right.normalized - moveVector.normalized;//计算从下一个方向向量指向当前向量的向量
if (vector.magnitude > 0.1f)
{
transform.Rotate(transform.forward, angel);
}
}
void Fire()
{
if (Input.GetKeyDown(KeyCode.Space))
{
var bulletDemo = Resources.Load<GameObject>("Bullet");
var bullet = Instantiate(bulletDemo, muzzle.transform);
bullet.transform.SetParent(bulletPiont.transform);
bullet.transform.localScale = Vector3.one;
//如果玩家数据的等级为4则改变自己的子弹效果,并将发射子弹的母体赋值给子弹
if (data.level == 4)
{
bullet.AddComponent<Bullet1>().motherObject = gameObject;
}
else
{
bullet.AddComponent<Bullet>().motherObject = gameObject;
}
}
}
// 死亡时将显示晚间外形组件隐藏,播放死亡动画0.3S后摧毁玩家组件
public void Die()
{
draw.gameObject.SetActive(false);
animator.Play("Die", 0);
StartCoroutine(OnDelay(0.3f, () =>
{
Destroy(gameObject);
}));
}
IEnumerator OnDelay(float time, UnityEngine.Events.UnityAction rFunc)
{
yield return new WaitForSeconds(time);
rFunc();
yield return null;
}
}
效果查看:

5.敌人
搭建好敌人模型,设置敌人Ai:
- 敌人可以上、下、左、右四个方向移动;
- 每次移动距离为场景中墙的1/2宽;
- 敌人无法穿越障碍;若没有遇见障碍则随机选择是否继续延当前方向前进,若遇见障碍则随机重新选择移动方向;
- 敌人可以发射子弹,频率为1.5s一次;
- 敌人可以死亡;
代码如下:
因为敌人和玩家基本雷同只是把玩家的输入换成了随机数,所以不贴代码了。
查看效果:

6.道具
搭建好道具模型,设置其功能
- 道具随机生成在地图上;
- 玩家获取道具后,道具消失;
因为只有一个道具,模块小没有单独划分模块出来随机生成的体现在GamaMode脚本中体现,玩家获取道具后消失在玩家功能中体现了。
嗯,90坦克的主要玩法差不多也就时这些了,而且所用的数据完全来至配置表,有兴趣的朋友可以自己研拓展,可以增加一些道具啊,或者玩家属性什么的,当然也可以加别的那就看你的脑洞了。
相关工程和插件已经上传github 链接 dingchao12/Deom.
转载于:https://zhuanlan.zhihu.com/p/31197620
网友评论