源码
体验App(密码8686)
先上效果图
思路
- 能移动的飞行物(View)
- 容纳飞行物的容器 (ViewGroup)
- 控制飞行物在容器中的位置、移动、以及碰撞检测
飞行物
首先捋一捋创建一个飞行物所需要的属性
- 类型(我的飞机、敌机、我的子弹、敌机子弹、BOSS飞机、BOSS子弹,为了方便我把爆炸效果也归类到飞行物中)
- 视图(View)
- 坐标
- 宽高
- 血量
- 威力(敌机撞到我,敌机子弹撞到我等等......)
- 飞行物的死亡标记
- 飞行物的飞行速度
- 在内部算出他的中心X和中心Y
- context
好的,捋清楚了可以开始写了
首先创建飞行物的基类
Fly
abstract class Fly {
//飞行物类型
var flyType: FlyType
//飞行物展示的view
var view: View
//飞行物的的偏移值 xy
var x = 0F
set(value) {
view.x = value
cx = (view.x + w / 2)
field = value
}
var y = 0F
set(value) {
view.y = value
cy = (view.y + h / 2)
field = value
}
//飞行物的宽高
var w = 0
var h = 0
//飞行物的血量
var HP = 100
//飞行物的碰撞威力
var power = 100
//这个飞行物是否已经死亡
var isBoom = false
//飞行物的移动速度 越小越快
var speed = 1
//上下文
var context: Context
//飞行物的中心点
var cx = 0F
var cy = 0F
}
这里飞行物基类创建好了之后开始派生子类
- 飞机(
Plane.kt
) - 子弹(
Bullet.kt
) - 爆炸(
Boom.kt
)
创建飞行物的枚举类型
FlyType
enum class FlyType {
PLANE_GCD, //我军飞机
PLANE_GMD, //敌军飞机
PLANE_BOSS, //boss飞机
BULLET_GCD, //我军子弹
BULLET_GMD, //敌军子弹
BULLET_BOSS,//boss子弹
BOOM, //爆炸类型
OTHER //预留
}
创建飞行物的视图(View)
作图是不可能作图的,这辈子都不可能作图,只能靠onDraw维持生活这样子
飞机
PlaneView
(我机和敌机都用这个view,如果是敌机就给他canvas.rotate(180)
)
竖着draw一个rect为飞机主体,横着draw两个rect为飞机的翅膀 ,再在尾部绘制火焰效果(火焰效果看这里,飞机就完成啦(点我看图
),代码我就不贴了。子弹
BulletView
(我机和敌机一样都用这个View)
竖着draw一个rect为子弹主体,横着draw一个rect为子弹尾翼,再在尾部绘制火焰效果爆炸效果
BoomView
实现的思路就是在view的中心绘制12个rect,之后用动画将这个12个rect沿着同一个方向移动,
在onDraw的过程中,每绘制一个之后,canvas.rotate(30)
,这样就有扩散效果了
容器
MapView
class MapView : ViewGroup {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
private var w = 0
private var h = 0
private var loadFinish = false
var onMeasureFinishListener: OnViewLoadFinishListener? = null
/**
* 指定飞行物的位置
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child != null) {
val childW = child.measuredWidth
val childH = child.measuredHeight
child.layout(0, 0, childW, childH)
}
}
}
/**
* 测量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
measureChildren(widthMeasureSpec, heightMeasureSpec)
w = measuredWidth
h = measuredHeight
if (!loadFinish) {
onMeasureFinishListener?.onFinish(w, h)
loadFinish = true
}
}
/**
* 添加飞行物
*/
fun addFly(fly: Fly) {
addView(fly.view)
}
/**
* 删除飞行物
*/
fun removeFly(fly: Fly) {
removeView(fly.view)
}
interface OnViewLoadFinishListener {
fun onFinish(w: Int, h: Int)
}
}
控制
首先提供一个简单的工厂类来获取飞机、子弹、爆炸
object FlyFactory{
/**
* 获取飞机的Fly
*/
fun getPlane(context: Context, flyType: FlyType): Plane?{......}
/**
* 获取子弹的Fly
*/
fun getBullet(context: Context, flyType: FlyType): Bullet?{......}
/**
* 获取爆炸效果的Fly
*/
fun getBoom(context: Context, flyType: FlyType): Boom? {......}
/**
* 获取爆炸View
*/
private fun getBoomView(context: Context, flyType: FlyType): View?{......}
/**
* 获取飞机或子弹的View
*/
private fun getView(context: Context, flyType: FlyType): View?{......}
}
主要的逻辑控制类FlyController
飞行物管理的集合
为了避免ConcurrentModificationException异常,这里用CopyOnWriteArrayList
/**
* 我军飞机
*/
private var gcdPlanes: CopyOnWriteArrayList = CopyOnWriteArrayList()
/**
* 敌军飞机集合
*/
private var gmdPlanes: CopyOnWriteArrayList = CopyOnWriteArrayList()
/**
* 我军子弹集合
*/
private var gcdBullets: CopyOnWriteArrayList = CopyOnWriteArrayList()
/**
* 敌军子弹集合
*/
private var gmdBullets: CopyOnWriteArrayList = CopyOnWriteArrayList()
创建飞机
private fun createPlane(flyType: FlyType) {
val plane = FlyFactory.getPlane(activity, flyType)!!
if (flyType == FlyType.PLANE_GCD) {
//我的飞机的y在屏幕的最下方,x在屏幕中心位置
plane.x = w / 2F - plane.w / 2
plane.y = h - plane.h.toFloat()
} else if (flyType == FlyType.PLANE_GMD) {
//敌机y在屏幕的最上方,x则是容器的w以内的随机位置
plane.x = (w - plane.w) * random.nextFloat()
plane.y = -plane.h * 2F
}
}
添加飞机
/**
* 添加Fly到List和mapView
*/
private fun addFly(fly: Fly) {
when (fly.flyType) {
FlyType.PLANE_GCD -> {
gcdPlanes.add(fly)
}
FlyType.PLANE_GMD -> {
gmdPlanes.add(fly)
}
......省略代码
}
mapView.addFly(fly)
}
删除飞行物
/**
* 从集合和mapView中删除fly
*/
private fun removeFly(fly: Fly) {
when (fly.flyType) {
FlyType.PLANE_GMD -> {
createPlane(FlyType.PLANE_GMD)
gmdPlanes.remove(fly)
}
FlyType.PLANE_GCD -> {
gcdPlanes.remove(fly)
}
......省略代码
}
mapView.removeFly(fly)
//删除的时候标记为死亡状态
fly.boom()
}
创建子弹
/**
*子弹的初始位置为发射子弹飞机的当前位置,所以需要传入plane
*/
private fun createBullet(plane: Plane) {
val bullet = FlyFactory.getBullet(activity, plane.flyType)!!
bullet.x = (plane.cx - bullet.w / 2)
bullet.y = (plane.cy - bullet.h / 2)
}
启动线程为飞机创建子弹
/**
* 我的飞机自动发射子弹线程
*/
private fun startGcdPlaneShotThread() {
Thread {
while (true) {
activity.runOnUiThread {
if (gcdPlanes.size != 0) {
shot(gcdPlanes[0] as Plane)
}
}
SystemClock.sleep(200)
}
}.start()
}
/**
* 敌机自动发射子弹线程
*/
private fun startGmdPlaneShotThread() {
Thread {
while (true) {
//遍历敌机集合,并随机确定是否为飞机创建子弹
gmdPlanes.forEach {
activity.runOnUiThread {
if (random.nextInt(3) % 3 == 0 )
shot(it as Plane)
}
}
SystemClock.sleep(2000)
}
}.start()
}
移动飞机和子弹
/**
* y轴移动Fly
*/
private fun moveFlyY(fly: Fly) {
//如果fly已经死亡
if (fly.isBoom) {
return
}
//移动的起始位置是自己的y
var start = fly.cy
//移动的目标位置,如果是敌机和敌机子弹,则是屏幕的最下方再加自身两个高度 ,我的子弹则相反
var end = when (fly.flyType) {
FlyType.BULLET_GCD -> {
-fly.h * 2
}
FlyType.PLANE_GCD -> {
return
}
FlyType.PLANE_GMD -> {
h + fly.h * 2
}
FlyType.BULLET_GMD -> {
h + fly.h * 2
}
......省略代码
}
ValueAnimator.ofFloat(start, end.toFloat())
.apply {
addUpdateListener {
//在动画过程中,如果经过碰撞检测判定飞行器已经死亡,则马上取消动画
if (fly.isBoom) {
cancel()
return@addUpdateListener
}
//更改飞行物的y值以移动飞行物
fly.y = (it.animatedValue as Float)
......省略代码
}
//动画的时间取值是起始距离到结束距离的值作为毫秒数,再乘以飞行器的速度
//speed越高则飞行越慢,在之前给敌机的speed赋值的时候可以使用随机数,让敌机飞行有快有慢
duration = (abs(start - end)).toLong() * fly.speed
interpolator = LinearInterpolator()
start()
}
}
飞出屏幕检测和碰撞检测
经过上面过程之后,飞机和子弹就动起来了。接下来在动画中,需要做飞出屏幕的检测和碰撞检测
首先飞出屏幕检测
/**
* 飞行物位置检测 如果已经超出屏幕则删除
*/
private fun checkFlyPosition(fly: Fly): Boolean {
//如果view已经不再屏幕内了 删除它
if (fly.x + fly.w <= -fly.w ||
fly.x >= w + fly.w ||
fly.y + fly.h <= -fly.h ||
fly.y >= h + fly.h
) {
removeFly(fly)
return false
}
return true
}
碰撞检测
/**
* 通过flyType来决定选择哪个集合中的元素与它进行碰撞检测
*/
private fun selectHitAndBeHit(fly: Fly) {
when (fly.flyType) {
FlyType.BULLET_GCD -> {
//如果是我的子弹,则与敌机集合检测
hitAndBeHit(fly, gmdPlanes)
}
FlyType.BULLET_GMD -> {
//如果是敌机的子弹则与我机检测
hitAndBeHit(fly, gcdPlanes)
}
FlyType.PLANE_GMD -> {
//...
hitAndBeHit(fly, gcdPlanes)
}
FlyType.PLANE_BOSS -> {
//...
hitAndBeHit(fly, gcdPlanes)
}
FlyType.BULLET_BOSS -> {
//...
hitAndBeHit(fly, gcdPlanes)
}
}
}
/**
* 碰撞和被碰撞的集合
*/
private fun hitAndBeHit(hitFly: Fly, beHitFlys: CopyOnWriteArrayList) {
for (beHitFly in beHitFlys) {
//碰撞之后跳出循环
if (isCollision(
hitFly.x,
hitFly.y,
hitFly.w.toFloat(),
hitFly.h.toFloat(),
beHitFly.x,
beHitFly.y,
beHitFly.w.toFloat(),
beHitFly.h.toFloat()
)
) {
sellingHP(hitFly, beHitFly)
break
}
}
}
/**
* 碰撞检测
*/
private fun isCollision(
x1: Float,
y1: Float,
w1: Float,
h1: Float,
x2: Float,
y2: Float,
w2: Float,
h2: Float
): Boolean {
if (x1 > x2 + w2 || x1 + w1 < x2 || y1 > y2 + h2 || y1 + h1 < y2) {
return false
}
return true
}
/**
* 碰撞扣除HP
*/
private fun sellingHP(fly1: Fly, fly2: Fly) {
fly1.HP -= fly2.power
fly2.HP -= fly1.power
isDie(fly1)
isDie(fly2)
}
/**
* 飞行物是否死亡
*/
private fun isDie(fly: Fly) {
if (fly.HP <= 0) {
//创建爆炸效果
createBoom(fly)
removeFly(fly)
}
}
创建爆炸效果
当碰撞发生时 如果有飞行器死亡,则会触发一个爆炸效果,位置就在飞行器死亡的位置
/**
* 创建爆炸效果
*/
private fun createBoom(fly: Fly) {
val boom = FlyFactory.getBoom(activity, fly.flyType)!!
boom.x = (fly.cx - boom.w / 2)
boom.y = (fly.cy - boom.h / 2)
mapView.addFly(boom)
val flyBoomView = boom.view as FlyBoomView
//爆炸动画结束的时候将其从mapView中删除
flyBoomView.animatorListener = object : FlyBoomView.AnimatorListener {
override fun onAnimationEnd() {
removeFly(boom)
}
}
}
添加Boss飞机
Boss飞机的创建与普通的飞机创建是一样的,传入不同的大小和颜色用来区分。Boss的y轴移动目标点只在屏幕的最上方,从负屏移动到0的位置即停止,并开始x轴的左右移动。需要要为Boss添加x轴的移动动画,以下代码
/**
* X轴移动Fly
*/
private fun moveFlyX(fly: Fly) {
//如果fly已经死亡
if (fly.isBoom) {
return
}
var start = fly.x
//左移动或者右移动
var end = if (fly.x < w - fly.w) {
w.toFloat() - fly.w
} else {
0F
}
ValueAnimator.ofFloat(start, end).apply {
addUpdateListener {
//如果fly已经死亡
if (fly.isBoom) {
cancel()
return@addUpdateListener
}
fly.x = it.animatedValue as Float
//超出屏幕检测
if (checkFlyPosition(fly)) {
selectHitAndBeHit(fly)
}
}
duration = (abs(start - end)).toLong() * fly.speed
interpolator = LinearInterpolator()
doOnEnd {
//递归调用,实现左右不停移动
moveFlyX(fly)
}
start()
}
}
滚动的星空背景
用自定义View不停的画圆实现
- 定义一个集合,准备存储圆的xy
- 心里将屏幕x轴定4个点,这四个点就是圆的中心
- 在屏幕的最上方,在x轴4个点随机取一个点,存入集合,并用这个点画一个圆。
- 接着开启一个动画,在动画中遍历集合累加y,这样圆就开始向下移动了
- 在移动的过程中判断y的移动距离,每到一定的距离就重复在最上方添加圆,这样就完成了
/**
* 滚动的星空背景
*/
class ScrollStarsView : BaseView {
/**
* 圆形集合
*/
private var circleStars = CopyOnWriteArrayList()
/**
* 圆形移动动画
*/
private var moveRectAnim: ValueAnimator? = null
private var random = Random()
/**
* 圆形直径
*/
private var diameter = 0
private var paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
}
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
diameter = (w / 3).toInt()
addRect()
moveRect()
}
/**
* 添加圆形
*/
private fun addRect() {
circleStars.add(
CircleStar(
(random.nextInt(4) * diameter).toFloat(),
-diameter.toFloat(),
diameter / 2F * (random.nextFloat() + 0.1F)
)
)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let { c ->
/**
* 绘制背景矩形
*/
paint.color = Color.parseColor("#0F2332")
c.drawRect(0F, 0F, w, h, paint)
/**
* 绘制圆形
*/
paint.color = Color.parseColor("#374060")
circleStars.forEach {
c.drawCircle(it.x, it.y, it.r, paint)
}
}
}
/**
* 移动圆形 每走diameter*2的距离 触发一次添加
*
*/
private fun moveRect() {
if (moveRectAnim == null) {
var oldAnimateValue = 0
moveRectAnim = ValueAnimator.ofInt(1, diameter * 2)
.apply {
addUpdateListener { it ->
val animateValue = it.animatedValue as Int
//repeat的时候重置oldAnimateValue
if (oldAnimateValue > animateValue) {
oldAnimateValue = 0
addRect()
}
circleStars.forEach {
it.y += animateValue - oldAnimateValue
//如果已经走出了屏幕外则删除它
if (it.y > h + diameter) {
circleStars.remove(it)
}
}
oldAnimateValue = animateValue
invalidate()
}
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
duration = 2000
start()
}
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
moveRectAnim?.cancel()
moveRectAnim = null
}
data class CircleStar(var x: Float, var y: Float, var r: Float)
}
完!