目录
使用VS传统方法制作
使用Unity制作
写在前面的话
C#可以干什么?
开发工具:Unity、VS
注意:杀毒软件可能会把开发完成阶段生成的exe文件误当成病毒删除,所以使用时注意关闭
一、准备
鼠标右键选择查看代码,可查看Form1.cs的具体代码
选择视图->工具箱,在工具箱中有一些系统自带组件鼠标拖动到Form1.cs进行UI布局的设计
控制窗体显示的位置
居中显示
自定义位置显示
我们找到Paint(这个事件是用于更新画布的),然后在其后面的空格处双击,然后我们就会得到一个Form1_Paint方法
下面我们在此方法中编写代码去画一条线段
查看本机有哪些字体?新建一个txt文件打开,然后找到字体即可查看
绘制文字
绘制图片,双击打开Resources文件,选择图像,选择添加现有文件,选择导入即可
我们可以在Resources类下发现有自动生成的代码
同理,添加音频
编写代码
绘制图片成功
控制代码收缩,使用region和endregion
也可用Bitmap来获取图片对象且使用它可以对颜色进行透明处理
二、正式开始
1.创建画布窗口
创建窗体应用项目,设置窗口居中显示,设置标题(长宽均为15*30像素,为了对此取奇数)和游戏标题
新建一个线程
创建Start和Update方法,Start方法用于游戏启动时的初始化,Update用于游戏每帧画面的更新逻辑操作
为了优化性能,限制1s执行60次update方法
我们在调试时可以发现,当关闭窗口后主线程没有关闭,这是因为子线程没有关闭的情况下子线程是不会关闭的,我们添加一个FormClosed事件(方法见一)
修改Thread作用域,然后在FormClosed方法中调用中断线程的方法
创建画布并赋值
将画布置为黑色 (为什么要把置为黑色代码放到每一帧里面重复执行?我直接执行一次不就好了吗?答案是:因为我们在游戏中还有动态的坦克,如果只执行一次则在创建坦克时会有重复)
2.绘制地图
创建GameObject类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class GameObject
{
public int X { get; set; }
public int Y { get; set; }
//上述等价于
//public int y;
//public int Y {
// get {
// return y;
// }
// set {
// value = y;
// }
//}
}
}
创建NotMovingThing类,继承GameObject,新建Image对象
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class NotMoveThing:GameObject
{
public Image img { get; set; }
}
}
创建MoveThing类
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
enum Direction{
UP,
DOWN,
LEFT,
RIGHT
}
class MoveThing:GameObject
{
public Bitmap BitmapUp { get; set; }
public Bitmap BitmapDown { get; set; }
public Bitmap BitmapLeft { get; set; }
public Bitmap BitmapRight { get; set; }
public int Speed { get; set; }
public Direction direction { get; set; }
}
}
创建MyTank、EnemyTank、Bullet,分别继承MoveThing
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class MyTank:MoveThing
{
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class EnemyTank:MoveThing
{
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class Bullet:MoveThing
{
}
}
为GameObject添加抽象方法以获取图片对象和在画布上画图片的公共方法
子类实现抽象方法,红线部分按下alt+enter选择实现抽象类,就会自动补充好实现方法,然后根据自己的逻辑需要修改即可
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class NotMoveThing:GameObject
{
public Image img { get; set; }
protected override Image GetImage()
{
return img;
}
}
}
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
enum Direction{
UP,
DOWN,
LEFT,
RIGHT
}
class MoveThing:GameObject
{
public Bitmap BitmapUp { get; set; }
public Bitmap BitmapDown { get; set; }
public Bitmap BitmapLeft { get; set; }
public Bitmap BitmapRight { get; set; }
public int Speed { get; set; }
public Direction direction { get; set; }
protected override Image GetImage()
{
switch (direction) {
case Direction.UP:
return BitmapUp;
case Direction.DOWN:
return BitmapDown;
case Direction.LEFT:
return BitmapLeft;
case Direction.RIGHT:
return BitmapRight;
default:
return BitmapUp;
}
}
}
}
将黑底图片设为透明
绘制墙,导入图片和音频资源(同上),创建GameObjectManager
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TankWar.Properties;
namespace TankWar
{
class GamebjectManager
{
private static List wallList = new List();//保存所有墙对象
public static void DrawMap() {
foreach (NotMoveThing wall in wallList) {
wall.DrawSelf();//绘制墙
}
}
public static void CreateMap() {
CreateWall(1, 1, 5,wallList);//创建墙对象
}
/**
* x,y代表一个30*30的方格的位置,如第一格是0,0,我们在绘画时从1,1位置开始画
* count代表要创建的强对象个数
*/
private static void CreateWall(int x,int y,int count,List wallList) {
int xPosition = x * 30;
int yPosition = y * 30;
for (int i=yPosition;i
新增NotMoveThing构造f方法
新增NotMoveThing构造方法
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class NotMoveThing:GameObject
{
public Image img { get; set; }
protected override Image GetImage()
{
return img;
}
public NotMoveThing(int x,int y,Image img) {
this.X = x;
this.Y = y;
this.img = img;
}
}
}
在GameFrameWork中调用
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class GameFramework
{
public static Graphics g;
public static void Start() {
GamebjectManager.CreateMap();
}
public static void Update() {
GamebjectManager.DrawMap();
}
}
}
效果如下,但是会出现闪烁问题(这是因为每一帧都需要重新绘制)
为了解决闪烁问题,我们可以采用把所有的图像绘制在一张图片上,然后再把图片绘制到画布上
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TankWar
{
public partial class Form1 : Form
{
private Thread thread;
private static Graphics windowG;//窗口画布对象
private static Bitmap tempBmp;//临时图片对象
public Form1()
{
InitializeComponent();
this.StartPosition = FormStartPosition.CenterScreen;//使窗口在屏幕居中显示
windowG = this.CreateGraphics();//创建窗体画布
tempBmp = new Bitmap(450,450);//创建临时图片
Graphics bmpG = Graphics.FromImage(tempBmp);//根据图片对象创建临时画布对象
GameFramework.g = bmpG;//赋值,以便在GameFramework中拿到此对象
thread = new Thread(new ThreadStart(GameMainThread));
thread.Start();
}
private static void GameMainThread() {
GameFramework.Start();
int sleepTime = 1000 / 60; //值为:1/60s
while (true)
{
GameFramework.g.Clear(Color.Black);//将画布内容清空,并置为黑色
GameFramework.Update();//画画
windowG.DrawImage(tempBmp, 0, 0);
Thread.Sleep(sleepTime);//每执行一次休息一段时间,保证1s执行60次
}
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
thread.Abort();
}
}
}
这样图像就不再闪动了
继续创建其他墙,并添加图片参数
继续添加墙和boss,最终代码和最终效果
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TankWar.Properties;
namespace TankWar
{
class GamebjectManager
{
private static List wallList = new List();//保存所有普通墙对象
private static List steelList = new List();//保存所有钢铁墙对象
private static NotMoveThing boss;//保存boss对象
public static void DrawMap() {
foreach (NotMoveThing wall in wallList) {
wall.DrawSelf();//绘制墙
}
foreach (NotMoveThing wall in steelList) {
wall.DrawSelf();//绘制墙
}
boss.DrawSelf();
}
public static void CreateMap() {
CreateWall(1, 1, 5,Resources.wall,wallList);//创建墙对象
CreateWall(3, 1, 5, Resources.wall, wallList);//创建墙对象
CreateWall(5, 1, 4, Resources.wall, wallList);//创建墙对象
CreateWall(7, 1, 3, Resources.wall, wallList);//创建墙对象
CreateWall(9, 1, 4, Resources.wall, wallList);//创建墙对象
CreateWall(11, 1, 5, Resources.wall, wallList);//创建墙对象
CreateWall(13, 1, 5, Resources.wall, wallList);//创建墙对象
CreateWall(7, 5, 1, Resources.steel, steelList);//创建钢铁墙对象
CreateWall(0, 7, 1, Resources.steel, steelList);//创建钢铁墙对象
CreateWall(14, 7, 1, Resources.steel, steelList);//创建钢铁墙对象
CreateWall(2, 7, 1, Resources.wall, wallList);
CreateWall(3, 7, 1, Resources.wall, wallList);
CreateWall(4, 7, 1, Resources.wall, wallList);
CreateWall(6, 7, 1, Resources.wall, wallList);
CreateWall(7, 6, 2, Resources.wall, wallList);
CreateWall(8, 7, 1, Resources.wall, wallList);
CreateWall(10, 7, 1, Resources.wall, wallList);
CreateWall(11, 7, 1, Resources.wall, wallList);
CreateWall(12, 7, 1, Resources.wall, wallList);
CreateWall(1, 9, 5, Resources.wall, wallList);//创建墙对象
CreateWall(3, 9, 5, Resources.wall, wallList);//创建墙对象
CreateWall(5, 9, 3, Resources.wall, wallList);//创建墙对象
CreateWall(6, 10, 1, Resources.wall, wallList);//创建墙对象
CreateWall(7, 10, 1, Resources.wall, wallList);//创建墙对象
CreateWall(8, 10, 1, Resources.wall, wallList);//创建墙对象
CreateWall(9, 9, 3, Resources.wall, wallList);//创建墙对象
CreateWall(11, 9, 5, Resources.wall, wallList);//创建墙对象
CreateWall(13, 9, 5, Resources.wall, wallList);//创建墙对象
CreateWall(6, 13, 2, Resources.wall, wallList);//创建墙对象
CreateWall(7, 13, 1, Resources.wall, wallList);//创建墙对象
CreateWall(8, 13, 2, Resources.wall, wallList);//创建墙对象
CreateBoss(7, 14,Resources.Boss);
}
/**
* x,y代表一个30*30的方格的位置,如第一格是0,0,我们在绘画时从1,1位置开始画
* count代表要创建的强对象个数
*/
private static void CreateWall(int x,int y,int count,Image img,List wallList) {
int xPosition = x * 30;
int yPosition = y * 30;
for (int i=yPosition;i
修改窗体属性,使其不能用鼠标拖动改变窗体大小,但可以最大化和最小化
3.绘制主角(即自己的坦克)
GameObjectManager类新增如下两个方法,并在GameFramework中添加调用
添加对应的构造方法
最终效果图:
4.控制坦克的移动
添加键盘监听事件函数(同上,再Form的属性->事件下找到KeyDown和KeyUp)
调用鼠标按下和起来的方法
这样控制坦克移动随可以,但会有一个一开始的卡顿(按下后移动一下然后停顿一s再往前走)
因此我们这样做,定义一个isMoving变量,当键盘按下后设置为true,当键盘起来时设置为false。然后根据isMoving来控制移动
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using TankWar.Properties;
namespace TankWar
{
class MyTank:MoveThing
{
public bool IsMoving { get; set; }
public MyTank(int x,int y,int speed) {
this.IsMoving = false;
this.X = x;
this.Y = y;
this.Speed = speed;
this.direction = Direction.UP;
BitmapDown = Resources.MyTankDown;
BitmapUp = Resources.MyTankUp;
BitmapRight = Resources.MyTankRight;
BitmapLeft = Resources.MyTankLeft;
}
public void KeyDown(KeyEventArgs args) {
switch (args.KeyCode) {
case Keys.W:
direction = Direction.UP;
IsMoving = true;
break;
case Keys.S:
direction = Direction.DOWN;
IsMoving = true;
break;
case Keys.A:
direction = Direction.LEFT;
IsMoving = true;
break;
case Keys.D:
direction = Direction.RIGHT;
IsMoving = true;
break;
}
}
public void KeyUp(KeyEventArgs args) {
switch (args.KeyCode)
{
case Keys.W:
IsMoving = false;
break;
case Keys.S:
IsMoving = false;
break;
case Keys.A:
IsMoving = false;
break;
case Keys.D:
IsMoving = false;
break;
}
}
private void Move() {
if (IsMoving==false) {
return;
}
switch (direction) {
case Direction.UP:
Y -= Speed;
break;
case Direction.DOWN:
Y += Speed;
break;
case Direction.LEFT:
X -= Speed;
break;
case Direction.RIGHT:
X += Speed;
break;
}
}
public override void Update()
{
Move();
base.Update();
}
}
}
现在我们的坦克可以移动了,但是会穿过墙体和外边界
添加墙体检测,在MoveCheck方法添加墙体检测
处理资源冲突异常:
在MoveThing中重写此方法
5.添加敌人坦克
添加EnemyTank生成方法
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using TankWar.Properties;
namespace TankWar
{
class GamebjectManager
{
...
private static List tankList = new List();//保存敌人坦克对象
private static int enemyBornSpeed = 60;//敌人坦克的生成速度
private static int enemyBornCount = 60;//敌人坦克的生成数量
private static Point[] points=new Point[3];
public static void Start() {
points[0].X = 0;
points[0].Y = 0;
points[1].X = 7*30;
points[1].Y = 0;
points[2].X = 14*30;
points[2].Y = 0;
}
public static void Update() {
...
foreach (EnemyTank tank in tankList) {
tank.Update();
}
...
EnemyBorn();
}
public static void EnemyBorn() {
enemyBornCount++;
if (enemyBornCount
创建EnemyTank构造函数及移动
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class EnemyTank:MoveThing
{
private Random r = new Random();
public EnemyTank(int x, int y, int speed,Bitmap bmpDown,Bitmap bmpUp,Bitmap bmpLeft,Bitmap bmpRight)
{
//this.IsMoving = true;
this.X = x;
this.Y = y;
this.Speed = speed;
BitmapDown = bmpDown;
BitmapUp = bmpUp;
BitmapRight = bmpRight;
BitmapLeft = bmpLeft;
this.Direction = Direction.DOWN;
}
public override void Update()
{
MoveCheck();//移动前检查
Move();
base.Update();
}
private void ChangeDirection() {
while (true) {
Direction dir = (Direction)r.Next(0, 4);
if (Direction == dir)
{
continue;
} else {
Direction = dir;
break;
}
}
MoveCheck();
}
private void Move()
{
switch (Direction)
{
case Direction.UP:
Y -= Speed;
break;
case Direction.DOWN:
Y += Speed;
break;
case Direction.LEFT:
X -= Speed;
break;
case Direction.RIGHT:
X += Speed;
break;
}
}
private void MoveCheck()
{
#region 检查是否超过窗体边界
if (Direction == Direction.UP)
{
if (Y - Speed < 0)
{
ChangeDirection();
return;
}
}
else if (Direction == Direction.DOWN)
{
if (Y + Speed + Height > 450)
{
ChangeDirection();
return;
}
}
else if (Direction == Direction.LEFT)
{
if (X - Speed < 0)
{
ChangeDirection();
return;
}
}
else if (Direction == Direction.RIGHT)
{
if (X + Speed + Width > 450)
{
ChangeDirection();
return;
}
}
#endregion
Rectangle rect = GetRectangle();
switch (Direction)
{
case Direction.UP:
rect.Y -= Speed;
break;
case Direction.DOWN:
rect.Y += Speed;
break;
case Direction.LEFT:
rect.X -= Speed;
break;
case Direction.RIGHT:
rect.X += Speed;
break;
}
if (GamebjectManager.isCollidedWall(rect) != null)
{
ChangeDirection();
return;
}
if (GamebjectManager.isCollidedSteel(rect) != null)
{
ChangeDirection();
return;
}
if (GamebjectManager.isCollidedBoss(rect))
{
ChangeDirection();
return;
}
}
}
}
6.发射子弹
在MyTank中添加空格监听,当按下空格后发射子弹
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TankWar.Properties;
namespace TankWar
{
enum Tag {
MyTank,
EnemyTank
}
class Bullet:MoveThing
{
public Tag Tag { get; set; } //用于判断是自己的坦克发射的子弹还是敌人的坦克发射的子弹
public bool isDestroy { get; set; }
public Bullet(int x, int y, int speed,Direction dir,Tag tag)
{
isDestroy = false;
this.X = x;
this.Y = y;
this.Speed = speed;
BitmapDown = Resources.BulletDown;
BitmapUp = Resources.BulletUp;
BitmapRight = Resources.BulletRight;
BitmapLeft = Resources.BulletLeft;
this.Direction = dir;
this.Tag = tag;
this.X -= Width / 2;
this.Y -= Height / 2;
}
public override void DrawSelf()
{
base.DrawSelf();
}
public override void Update()
{
MoveCheck();//移动前检查
Move();
base.Update();
}
private void Move()
{
switch (Direction)
{
case Direction.UP:
Y -= Speed;
break;
case Direction.DOWN:
Y += Speed;
break;
case Direction.LEFT:
X -= Speed;
break;
case Direction.RIGHT:
X += Speed;
break;
}
}
private void MoveCheck()
{
#region 检查是否超过窗体边界
if (Direction == Direction.UP)
{
if (Y +Height/2+3< 0)//子弹图片自身的高度/2和子弹本身的高度的一半(大约为3)
{
isDestroy=true;
return;
}
}
else if (Direction == Direction.DOWN)
{
if (Y + Height/2 -3 > 450)
{
isDestroy = true;
return;
}
}
else if (Direction == Direction.LEFT)
{
if (X+Width/2-3 < 0)
{
isDestroy = true;
return;
}
}
else if (Direction == Direction.RIGHT)
{
if (X+Width/2+3 > 450)
{
isDestroy = true;
return;
}
}
#endregion
...
}
}
}
当子弹超出边界时,判断isDestroy并销毁
当子弹遇到墙或者敌人后销毁它们
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TankWar.Properties;
namespace TankWar
{
enum Tag {
MyTank,
EnemyTank
}
class Bullet:MoveThing
{
...
private void MoveCheck()
{
...
Rectangle rect = GetRectangle();
rect.X = X + Width / 2 - 3;//取到子弹实际位置的左上角横坐标
rect.Y = Y + Height / 2 - 3;
rect.Height = 3;
rect.Width = 3;
NotMoveThing wall = null;
if ((wall=GamebjectManager.isCollidedWall(rect)) != null)
{
isDestroy = true;
GamebjectManager.DestoryWall(wall);
return;
}
if (GamebjectManager.isCollidedSteel(rect) != null)
{
isDestroy = true;
return;
}
if (GamebjectManager.isCollidedBoss(rect))
{
//ChangeDirection();
return;
}
if (Tag==Tag.MyTank) {
EnemyTank tank = null;
if ((tank = GamebjectManager.isCollidedEnemyTank(rect))!=null)
{
isDestroy = true;
GamebjectManager.DestoryTank(tank);
return;
}
}
}
}
}
效果
7.添加爆炸效果
创建爆炸效果类
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TankWar.Properties;
namespace TankWar
{
class Explosion : GameObject
{
public bool IsNeedDestory { get; set;}
private int playSpeed = 1;
private int playCount = 0;//0/2=0 1/2=0 2/2=1 3/2=1..每张图片停留2帧
private int index = 0;
private Bitmap[] bmpArray = new Bitmap[] {
Resources.EXP1,
Resources.EXP2,
Resources.EXP3,
Resources.EXP4,
Resources.EXP5
};
public Explosion(int x,int y) {
foreach (Bitmap bmp in bmpArray) {
bmp.MakeTransparent(Color.Black);
}
this.X = x - bmpArray[0].Width / 2;//得到左上角坐标
this.Y = y - bmpArray[0].Height / 2;
IsNeedDestory = false;
}
protected override Image GetImage()
{
if (index>4) {
return bmpArray[4];
}
return bmpArray[index];
}
public override void Update()
{
playCount++;
index = (playCount - 1) / playSpeed;//获取播放图片的索引
if (index>4) {
IsNeedDestory = true;
}
base.Update();
}
}
}
添加生成爆炸效果的代码
8.优化敌人坦克
敌人坦克可发射子弹
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TankWar
{
class EnemyTank:MoveThing
{
public int AttackSpeed { get; set; }
private int attackCount = 0;
...
public EnemyTank(int x, int y, int speed,Bitmap bmpDown,Bitmap bmpUp,Bitmap bmpLeft,Bitmap bmpRight)
{
...
AttackSpeed = 60;
}
public override void Update()
{
...
AttackCheck();//是否需要攻击
...
}
...
private void AttackCheck() {
attackCount++;
if (attackCount < AttackSpeed) return;
Attack();
attackCount = 0;
}
private void Attack()
{
int x = this.X;
int y = this.Y;
switch (Direction)
{
case Direction.UP:
x = x + Width / 2;
break;
case Direction.DOWN:
x = x + Width / 2;
y += Height;
break;
case Direction.LEFT:
y = y + Height / 2;
break;
case Direction.RIGHT:
x += Width;
y = y + Height / 2;
break;
}
GamebjectManager.CreateBullet(x, y, Tag.EnemyTank, Direction);
}
}
}
敌人可随机转向,而不是遇到障碍我才转向
子弹可以攻击主角坦克,主角坦克可以被攻击4次,第4次主角坦克消失并回归起点
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using TankWar.Properties;
namespace TankWar
{
class MyTank:MoveThing
{
public int HP { get; set; }//血量
...
private int originalX, originalY;
public MyTank(int x,int y,int speed) {
...
originalX = x;
originalY = y;
...
HP = 4;
}
...
public void TakeDamage() {
HP--;
if (HP<=0) {
X = originalX;
Y = originalY;
HP = 4;
}
}
}
}
子弹攻击boss时游戏结束
9.添加音效
using System;
using System.Collections.Generic;
using System.Linq;
using System.Media;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TankWar.Properties;
namespace TankWar
{
class SoundManager
{
private static SoundPlayer startPlayer = new SoundPlayer();
private static SoundPlayer addPlayer = new SoundPlayer();
private static SoundPlayer blastPlayer = new SoundPlayer();
private static SoundPlayer firePlayer = new SoundPlayer();
private static SoundPlayer hitPlayer = new SoundPlayer();
public static void InitSound() {
startPlayer.Stream = Resources.start;
addPlayer.Stream = Resources.add;
blastPlayer.Stream = Resources.blast;
firePlayer.Stream = Resources.fire;
hitPlayer.Stream = Resources.hit;
}
public static void PlayStart() {
startPlayer.Play();
}
public static void PlayAdd()
{
addPlayer.Play();
}
public static void PlayBlast() {
blastPlayer.Play();
}
public static void PlayFire()
{
firePlayer.Play();
}
public static void PlayHit()
{
hitPlayer.Play();
}
}
}
1.创建工程
修改布局模式为2 by 3
Project面板切换单行模式
导入资源(将unitypackage文件拖到Project面板,在弹出的弹窗点击Import即可)
确保单张图片的SpriteMode选择为Single,多张小图片组成的图片选择为Multiple,TextureType选择为Sprite2D
切割图片(点击Sprite Editor->Slice->Gride By Cell Size,输入最小图片大小点击Slice即可,之后就可以展开看到切割后图片)
创建Player(将Player1拖到Hierarchy面板即可),创建其对应预制体,同理可创建Wall、Barrier、Grass、Heart
创建动画效果(出生动画、爆炸动画、护盾动画、河流动画),选择动画然后拖到Hierarchy面板,然后命名即可,然后创建其对于预制体,然后会自动产生俩个文件,将其放到新建文件夹Animator和AnimatorController中(适当改名以清晰结构)
2.控制Player移动
新建脚本Player放在新建文件夹Scripts下,与Player对象关联,然后编辑内容
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public float moveSpeed=3;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float h = Input.GetAxisRaw("Horizontal");
transform.Translate(Vector3.right * h * moveSpeed * Time.deltaTime,Space.World);//乘以delTatime表示按每一秒移动,以世界坐标轴移动
float v = Input.GetAxisRaw("Vertical");
transform.Translate(Vector3.up * v * moveSpeed * Time.deltaTime, Space.World);
}
}
新建一个数组,用以存放4个方向的图片
获取SpriteRender对象控制图片的显示
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public float moveSpeed=3;
private SpriteRenderer sr;
public Sprite[] tankSprite;
private void Awake()
{
sr = GetComponent();//得到图片渲染组件
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
float h = Input.GetAxisRaw("Horizontal");
transform.Translate(Vector3.right * h * moveSpeed * Time.deltaTime,Space.World);//乘以delTatime表示按每一秒移动,以世界坐标轴移动
if (h < 0) {
sr.sprite = tankSprite[3];//左
} else if (h>0) {
sr.sprite = tankSprite[1];//右
}
float v = Input.GetAxisRaw("Vertical");
transform.Translate(Vector3.up * v * moveSpeed * Time.deltaTime, Space.World);
if (v < 0)
{
sr.sprite = tankSprite[2];
}
else if (v > 0)
{
sr.sprite = tankSprite[0];
}
}
}
3.为Player添加碰撞效果
添加碰撞器(点击Add Component后如图)
添加刚体组件
然后将Player的所有新加属性应用到其预制体上
然后为剩余的Map预制体添加碰撞器(这里把River换到了Map文件中,Player放在了新文件夹下)
然后在运行后发现坦克下落了(这是因为重力的原因,我们将其重力设为0即可)
去掉Grass的碰撞器,因为逻辑上不需要碰撞
当我们在运行时会发现坦克会在碰撞体边角处发生z轴的旋转,所以我们在这里勾选如图选项即可
处理坦克遇到墙面时抖动滚动的情形(将所有代码放大FixedUpdate方法中并改Time.DeltaTime为Time.fixedDeltaTime,即固定每一帧执行的时间从而保证物理碰撞时相同的)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public float moveSpeed=3;
private SpriteRenderer sr;
public Sprite[] tankSprite;
private void Awake()
{
sr = GetComponent();//得到图片渲染组件
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void FixedUpdate()
{
float h = Input.GetAxisRaw("Horizontal");
transform.Translate(Vector3.right * h * moveSpeed * Time.deltaTime, Space.World);//乘以delTatime表示按每一秒移动,以世界坐标轴移动
if (h < 0)
{
sr.sprite = tankSprite[3];//左
}
else if (h > 0)
{
sr.sprite = tankSprite[1];//右
}
float v = Input.GetAxisRaw("Vertical");
transform.Translate(Vector3.up * v * moveSpeed * Time.deltaTime, Space.World);
if (v < 0)
{
sr.sprite = tankSprite[2];
}
else if (v > 0)
{
sr.sprite = tankSprite[0];
}
}
}
在测试过程种,我们发现同时按下两个键(如左上、左下等)坦克会歇着走,这样是不好的用户体验,所以我们添加如下内容,同时为了代码简洁我们把它放到一个Move方法中调用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
...
private void FixedUpdate()
{
Move();
}
private void Move()
{
float h = Input.GetAxisRaw("Horizontal");
transform.Translate(Vector3.right * h * moveSpeed * Time.fixedDeltaTime, Space.World);//乘以delTatime表示按每一秒移动,以世界坐标轴移动
if (h < 0)
{
sr.sprite = tankSprite[3];//左
}
else if (h > 0)
{
sr.sprite = tankSprite[1];//右
}
if (h != 0)
{
return;//处理两键同时按下导致坦克斜着走问题
}
float v = Input.GetAxisRaw("Vertical");
transform.Translate(Vector3.up * v * moveSpeed * Time.fixedDeltaTime, Space.World);
if (v < 0)
{
sr.sprite = tankSprite[2];
}
else if (v > 0)
{
sr.sprite = tankSprite[0];
}
}
}
设置层级显示(即多张图片叠在一起后优先显示那张图片),下图将出生动画层级设为1(默认为0)则坦克图片经过时就会被遮盖了,同理可设置Grass、Explosion等
然后我们添加Bullet(子弹),将图片拖入左侧面板即可,然后再创建其预制体(放在Tank下)
添加发射子弹的函数Attack,并绑定预制体
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
...
public GameObject bulletPrefab;//子弹预制体
private void Awake()
{
sr = GetComponent();//得到图片渲染组件
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
Attack(); //发射子弹,一定要放在Update中,如果放在FixedUpdate会偶尔发出不出子弹
}
private void FixedUpdate()
{
Move(); //坦克移动
}
private void Attack() {
if (Input.GetKeyDown(KeyCode.Space)) {
Instantiate(bulletPrefab, transform.position, transform.rotation);
}
}
...
}
控制子弹的角度(这里需要把欧拉角转换为四元数表示传入),然后再每次坦克变向时设置bulletAngle
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
...
private Vector3 bulletAngle;//子弹的发射角度
...
// Update is called once per frame
void Update()
{
Attack(); //发射子弹
}
private void FixedUpdate()
{
Move(); //坦克移动
}
private void Attack() {
if (Input.GetKeyDown(KeyCode.Space)) {
Instantiate(bulletPrefab, transform.position, Quaternion.Euler(transform.eulerAngles+bulletAngle));
}
}
private void Move()
{
float v = Input.GetAxisRaw("Vertical");
transform.Translate(Vector3.up * v * moveSpeed * Time.fixedDeltaTime, Space.World);
if (v < 0)
{
sr.sprite = tankSprite[2];
bulletAngle = new Vector3(0, 0, -180);
}
else if (v > 0)
{
sr.sprite = tankSprite[0];
bulletAngle = new Vector3(0, 0, 0);
}
if (v != 0)
{
return;//处理两键同时按下导致坦克斜着走问题
}
float h = Input.GetAxisRaw("Horizontal");
transform.Translate(Vector3.right * h * moveSpeed * Time.fixedDeltaTime, Space.World);//乘以delTatime表示按每一秒移动,以世界坐标轴移动
if (h < 0)
{
sr.sprite = tankSprite[3];//左
bulletAngle = new Vector3(0, 0, 90);//这里记住坐标是反着的,
}
else if (h > 0)
{
sr.sprite = tankSprite[1];//右
bulletAngle = new Vector3(0, 0, -90);
}
}
}
欧拉角:欧拉角包括3个旋转,根据这3个旋转来指定一个刚体的朝向。这3个旋转分别绕x轴,y轴和z轴,分别称为Pitch,Yaw和Roll
绕X轴(红线旋转)
绕Y轴(绿线)旋转
绕Z轴(蓝线)旋转
与我们在Unity的坐标一一对应
四元数
这个四元数真的很难理解,我们先来看一个我们好理解的二元数(也即我们学过的复数),如图我们画一条线段AB(用3+4i表示从A点到B点的路程变化,而B的坐标为(3,4)),当我们想把这条线段旋转90度时,我们得到-6+4i,从而达到旋转后B点的坐标为(-6,4),这并非巧合,而是可通过计算得到:(4+6i)*i=4i-6,即乘以i代表旋转90度。如果想旋转45度呢?乘以 就可以得到(即旋转度数为,乘以cos+sini)
然后我们推广到三维空间,同样的几何意义,一个空间线段旋转指定度数得到新的坐标,虚四元数表示为:q=q0+q1i+q2j+q3k,其中,则旋转后的四元数记为p=(q0+q1i+q2j+q3k)*(cos +sin i+sin j+sin k)即可得到了
控制子弹的移动,创建Bullet脚本,并放在Scripts下,并于Bullet预制体绑定
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float moveSpeed = 10;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.Translate(transform.up * moveSpeed * Time.deltaTime, Space.World);//世界坐标轴
}
}
为子弹添加CD(不添加会随这快速按键发射太快)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
...
private float timeVal;//发射子弹CD
// Update is called once per frame
void Update()
{
Attack();
if (timeVal >= 0.4){
Attack(); //发射子弹
}else {
timeVal += Time.deltaTime;
}
}
private void FixedUpdate()
{
Move(); //坦克移动
}
private void Attack() {
if (Input.GetKeyDown(KeyCode.Space)) {
Instantiate(bulletPrefab, transform.position, Quaternion.Euler(transform.eulerAngles+bulletAngle));
timeVal = 0;
}
}
...
}
为子弹添加触发器(记得勾选Is Trigger)和钢体组件(重力设为0)
为预制体添加Tag,并修改对应Player、Barrier、Wall的Tag
创建空气墙(为了给四周做一个边界,从而判断子弹何时被销毁)复制一个Barrier取名为AirBarrier,删除其SpriteRenderer组件(这样它就变透明不会渲染样式了,即看不见的墙)然后创建其对应预制体
添加坦克开始时的护盾效果,将护盾效果放在Player下面(这样就会随着坦克移动了),然后在Player脚本添加护盾效果预制体(记得绑定)、护盾效果时间和是否保护标志等逻辑代码(当护盾时间不大于0时隐藏护盾效果)最后将Player的属性Apply到预制体上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
...
private float defendTimeVal=3;//保护时间
private bool isDefended=true;//是否保护
...
public GameObject defendEffectPrefab;//护盾特效预制体
...
// Update is called once per frame
void Update()
{
//是否处于无敌状态
if (isDefended) {
defendEffectPrefab.SetActive(true);
defendTimeVal -= Time.deltaTime;
if (defendTimeVal<=0) {
isDefended = false;
defendEffectPrefab.SetActive(false);
}
}
if (timeVal >= 0.4)
{
Attack(); //发射子弹
}
else
{
timeVal += Time.deltaTime;
}
}
}
添加Boos的破坏效果,新建Heart脚本,并于Heart预制体绑定,编码,然后绑定图片
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Heart : MonoBehaviour
{
private SpriteRenderer sr;
public Sprite BrokenSprite;
// Start is called before the first frame update
void Start()
{
sr= GetComponent();
}
// Update is called once per frame
void Update()
{
}
public void Die() {
sr.sprite = BrokenSprite;
}
}
为子弹创建触发方法(针对Wall、Heart、Barrier)(Barrier的IsPlayerBullet要勾选)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float moveSpeed = 10;
public bool isPlayerBullet;//是否为玩家子弹
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.Translate(transform.up * moveSpeed * Time.deltaTime, Space.World);//世界坐标轴
}
private void OnTriggerEnter2D(Collider2D collision)
{
switch (collision.tag) {
case "Tank":
if (!isPlayerBullet) {
collision.SendMessage("Die");//执行碰撞到的物体的Die方法
}
break;
case "Heart":
collision.SendMessage("Die");//执行碰撞到的物体的Die方法
Destroy(gameObject);//销毁子弹自身
break;
case "Enemy":
break;
case "Wall":
Destroy(collision.gameObject);//销毁碰撞到的物体
Destroy(gameObject);//销毁子弹自身
break;
case "Barrier":
Destroy(gameObject);//销毁子弹自身
break;
default:
break;
}
}
}
重命名子弹名称,并新建一个EnemyBullet,然后其IsPlayerBullet取消勾选,PlayerBullet则勾选
创建爆炸特效脚本并与预制体绑定
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Explosion : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Destroy(gameObject, 0.2f);
}
// Update is called once per frame
void Update()
{
}
}
创建出生效果,创建Born脚本(与Born预制体绑定),然后将Player预制体与脚本绑定
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Born : MonoBehaviour
{
public GameObject playerPrefab;
// Start is called before the first frame update
void Start()
{
Invoke("BornTank", 1f);//延时调用
Destroy(gameObject, 1f);//延时销毁
}
// Update is called once per frame
void Update()
{
}
private void BornTank() {
Instantiate(playerPrefab, transform.position, Quaternion.identity);
}
}
创建敌人(设置钢体、碰撞器、重力为0,Z轴定向,设置4各方向图片、创建对应预制体,绑定爆炸效果预制体和子弹预制体)添加移动AI(每3秒进行一次攻击,每4秒进行一次转向),并添加敌人子弹的判定效果
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
public float moveSpeed = 3;
private Vector3 bulletAngle;//子弹的发射角度
private float v, h;
private float timeVal;//发射子弹CD
private float timeValChangeDirection=4;//改变方向的时间
private SpriteRenderer sr;
public Sprite[] tankSprite;
public GameObject bulletPrefab;//子弹预制体
public GameObject explosionPrefab;//爆炸效果预制体
private void Awake()
{
sr = GetComponent();//得到图片渲染组件
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (timeVal >= 3)
{
Attack(); //发射子弹
}
else
{
timeVal += Time.deltaTime;
}
}
private void FixedUpdate()
{
Move(); //坦克移动
}
private void Attack()
{
Instantiate(bulletPrefab, transform.position, Quaternion.Euler(transform.eulerAngles + bulletAngle));
timeVal = 0;
}
private void Move()
{
if (timeValChangeDirection >= 4)
{
int num = Random.Range(0, 8);
if (num > 5)
{
v = -1;//向下走
h = 0;
}
else if (num == 0)
{
v = 1;//向后走
h = 0;
}
else if (num > 0 && num <= 2)
{
h = -1;//向左走
v = 0;
}
else if (num > 2 && num <= 4)
{
h = 1;//向右走
v = 0;
}
timeValChangeDirection = 0;
} else {
timeValChangeDirection += Time.fixedDeltaTime;
}
transform.Translate(Vector3.right * h * moveSpeed * Time.fixedDeltaTime, Space.World);//乘以delTatime表示按每一秒移动,以世界坐标轴移动
if (h < 0)
{
sr.sprite = tankSprite[3];//左
bulletAngle = new Vector3(0, 0, 90);//这里记住坐标是反着的,
}
else if (h > 0)
{
sr.sprite = tankSprite[1];//右
bulletAngle = new Vector3(0, 0, -90);
}
if (h != 0)
{
return;//处理两键同时按下导致坦克斜着走问题
}
transform.Translate(Vector3.up * v * moveSpeed * Time.fixedDeltaTime, Space.World);
if (v < 0)
{
sr.sprite = tankSprite[2];
bulletAngle = new Vector3(0, 0, -180);
}
else if (v > 0)
{
sr.sprite = tankSprite[0];
bulletAngle = new Vector3(0, 0, 0);
}
}
private void Die()
{
Instantiate(explosionPrefab, transform.position, transform.rotation);//产生爆炸效果
Destroy(gameObject);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float moveSpeed = 10;
public bool isPlayerBullet;//是否为玩家子弹
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.Translate(transform.up * moveSpeed * Time.deltaTime, Space.World);//世界坐标轴
}
private void OnTriggerEnter2D(Collider2D collision)
{
switch (collision.tag) {
case "Tank":
if (!isPlayerBullet) {
collision.SendMessage("Die");//执行碰撞到的物体的Die方法
Destroy(gameObject);//销毁子弹自身
}
break;
case "Heart":
collision.SendMessage("Die");//执行碰撞到的物体的Die方法
Destroy(gameObject);//销毁子弹自身
break;
case "Enemy":
if (isPlayerBullet) {
collision.SendMessage("Die");
Destroy(gameObject);//销毁子弹自身
}
break;
case "Wall":
Destroy(collision.gameObject);//销毁碰撞到的物体
Destroy(gameObject);//销毁子弹自身
break;
case "Barrier":
Destroy(gameObject);//销毁子弹自身
break;
default:
break;
}
}
}
创建多个Born(绑定敌人坦克预制体,多个Born中有一个是用来生成Player的,所以需要勾选CreatePlayer)编写代码控制显示和销毁以及产生的坦克类型
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Born : MonoBehaviour
{
public GameObject playerPrefab;
public GameObject[] enemyPrefabList;
public bool createPlayer;
// Start is called before the first frame update
void Start()
{
Invoke("BornTank", 1f);//延时调用
Destroy(gameObject, 1f);//延时销毁
}
// Update is called once per frame
void Update()
{
}
private void BornTank() {
if (createPlayer) {
Instantiate(playerPrefab, transform.position, Quaternion.identity);
}else {
int num = Random.Range(0, 2);
Instantiate(enemyPrefabList[num], transform.position, Quaternion.identity);
}
}
}
创建空对象命名为MapCreation,创建MapCreation脚本,声明一个GameObject数组,将下图种相关预制体拖入(右上角的锁可以锁定该页面方便拖放预制体)
创建BOSS和BOSS周围的墙
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapCreation : MonoBehaviour
{
public GameObject[] item;//保存Boss、墙、障碍、出生效果、河流、草、空气墙
private void Awake()
{
//在界面下边界中间位置,创建Boss
CreateItem(item[0], new Vector3(0, -8, 0), Quaternion.identity);
//Boss周围的墙
CreateItem(item[1], new Vector3(-1,-8,0), Quaternion.identity);
CreateItem(item[1], new Vector3(1, -8, 0), Quaternion.identity);
for (int i=-1;i<2;i++) {
CreateItem(item[1], new Vector3(i, -7, 0), Quaternion.identity);
}
}
private void CreateItem(GameObject createObject,Vector3 position,Quaternion rotation) {
GameObject item = Instantiate(createObject, position, rotation);
item.transform.SetParent(gameObject.transform);//将新建的对象放在MapCreation下使目录简洁
}
}
创建外围空气墙,随机位置创建其他对象(墙、障碍、海、草等),拖入一个主角坦克生成点Born
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapCreation : MonoBehaviour
{
public GameObject[] item;//保存Boss、墙、障碍、出生效果、河流、草、空气墙
private List itemPostionList=new List();//保存每个对象的位置,用于判断随机生成的位置是否已有对象
private void Awake()
{
//在界面下边界中间位置,创建Boss
CreateItem(item[0], new Vector3(0, -8, 0), Quaternion.identity);
//Boss周围的墙
CreateItem(item[1], new Vector3(-1,-8,0), Quaternion.identity);
CreateItem(item[1], new Vector3(1, -8, 0), Quaternion.identity);
for (int i=-1;i<2;i++) {
CreateItem(item[1], new Vector3(i, -7, 0), Quaternion.identity);
}
//创建外围空气墙
for (int i=-11;i<12;i++) {//上边界
CreateItem(item[6], new Vector3(i, 9, 0), Quaternion.identity);
}
for (int i = -11; i < 12; i++)//下边界
{
CreateItem(item[6], new Vector3(i, -9, 0), Quaternion.identity);
}
for (int i = -8; i < 9; i++)//左边界
{
CreateItem(item[6], new Vector3(-11, i, 0), Quaternion.identity);
}
for (int i = -8; i < 9; i++)//右边界
{
CreateItem(item[6], new Vector3(11, i, 0), Quaternion.identity);
}
//创建其他对象(墙、障碍、河流、草)
for (int i=0;i<20;i++) {
CreateItem(item[1], createRandomPosition(), Quaternion.identity);
}
for (int i = 0; i < 20; i++)
{
CreateItem(item[2], createRandomPosition(), Quaternion.identity);
}
for (int i = 0; i < 20; i++)
{
CreateItem(item[4], createRandomPosition(), Quaternion.identity);
}
for (int i = 0; i < 20; i++)
{
CreateItem(item[5], createRandomPosition(), Quaternion.identity);
}
}
private void CreateItem(GameObject createObject,Vector3 position,Quaternion rotation) {
GameObject item = Instantiate(createObject, position, rotation);
item.transform.SetParent(gameObject.transform);//将新建的对象放在MapCreation、下
itemPostionList.Add(position);//将新建的对象位置放入列表
}
private Vector3 createRandomPosition() {
while (true) {
Vector3 position = new Vector3(Random.Range(-9, 10), Random.Range(-7, 8), 0);//不在四个边界产生游戏物体
if (!IsUsedPosition(position)) {
return position;
}
}
}
private bool IsUsedPosition(Vector3 position) {
for (int i=0;i