自定義View6 -塔防小遊戲:第三篇防禦塔隨意放置+多組野怪

2022-10-05 06:01:31

第一篇:一個防禦塔+多個野怪(簡易版)
第二篇:防禦塔隨意放置
第三篇:防禦塔隨意放置+多組野怪

  1、動態addView防禦塔

  2、防禦塔放置後不可以移動

  3、彎曲道路

  4、素材替換

第四篇:多波野怪

第五篇:殺死野怪獲得金幣

第六篇:防禦塔可升級,增強攻擊力,增大射程

描述:防禦塔可以放置多個,每一個都是獨立的,他們的攻擊互不影響(防禦塔隨意拖動在第二篇),這裡用到的知識是,自定義view的拖動,防禦塔是否可以攻擊的計算,防禦塔的攻擊路徑。

1、放置防禦塔

  • 新建類ActivityTower5,主要控制放置塔的回撥
  • 新建BattlefieldView5,主要渲染戰場
  • 新建TowerView5,主要繪製防禦塔,(其實野怪也需要單獨建立view)

1.1ActivityTower5首頁該做些什麼?

這次我們想要做成動態的,由使用者自行開啟,玩累了還能暫停,而且有錢可以建立多個防禦塔(後續加入攻擊野怪獲得金幣),所以建立開啟按鈕,暫停按鈕,建立A炮(後續有B炮,C炮...),程式碼如下

<?xml version="1.0" encoding="utf-8"?>
<layout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout_relative"
    ......
    android:gravity="center"
    ......>

    <com.liu.lib_view.tower.tower4.BattlefieldView4
        android:id="@+id/TowerView"
        ......
        />
    <LinearLayout
        android:id="@+id/bottom"
        ......>
        <Button
            android:id="@+id/start"
            ......
            android:text="開始"/>
        <Button
            android:id="@+id/pause"
            ......
            android:text="暫停"/>
        <Button
            android:id="@+id/create"
            ......
            android:text="建立A炮"/>
    </LinearLayout>
</RelativeLayout>
</layout>

這次新增一些素材,這些都是在網上隨便找的,一個背景圖片,一個防禦塔,一個野怪,這次做成橫屏的,我們需要記錄一下彎曲道路的xy座標,封裝成一個list(下面有解)。

1.2、BattlefieldView5渲染戰場

整合ViewGroup,因為我們要在裡面新增其他View,只有ViewGroup才有addView方法,這裡我們宣告一些屬性,妖怪大道、野怪、防禦塔畫筆這些必不可少,我們這次是多個防禦塔就要建立towerList來儲存我們建立的防禦塔,野怪數量也是如此。

注意:整合ViewGroup這裡要寫setWillNotDraw方法,不然onDraw()不執行。

我們設定完背景圖片後,開始渲染戰場,首先繪製道路,這次是彎曲的,會用到Path類,

  •   moveTo(x,y)  移動的起始點
  •   lineTo(x,y)  從起始點到該點畫一條線。

我們按照背景圖路線琢磨一下路線座標(每個手機可能存在差異),大概是如下

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    towerX = w / 2;
    towerY = h / 2;

    roadPath = new Path();
    roadPath.moveTo(0,900);
    roadPath.lineTo(500,900);
    roadPath.lineTo(500,200);
    roadPath.lineTo(800,200);
    roadPath.lineTo(800,800);
    roadPath.lineTo(1600,800);
    roadPath.lineTo(1600,200);
    roadPath.lineTo(towerX*2,200);
}

大體路線已經出來了,我們需要獲取這條線上的座標點,pathMeasure類可以獲取path路徑上的點數

原始碼解析

pathMeasure.getLength()獲取點數
pathMeasure.getPosTan(距離,pos,tan);
原始碼解釋:
   * @param distance The distance along the current contour to sample 沿輪廓到樣本的距離
     * @param pos If not null, returns the sampled position (x==[0], y==[1])
     * @param tan If not null, returns the sampled tangent (x==[0], y==[1])
     * @return false if there was no path associated with this measure object
    */
    public boolean getPosTan(float distance, float pos[], float tan[]) {
        if (pos != null && pos.length < 2 ||
            tan != null && tan.length < 2) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return native_getPosTan(native_instance, distance, pos, tan);
    }
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    ......
    pathMeasure = new PathMeasure();
    pathMeasure.setPath(roadPath,false);
}

 到這裡,我們就可以拿到路徑了,我們還需要把妖怪大道path集合存入到每個野怪屬性裡,讓野怪沿著這條路線走,

public class BlameBean {
    private int blameId;
    public int x;
    public int y;
    public int speed;//行走速度
    public int HP;//血量
    public boolean isAttacks;//是否可以被攻擊
    public boolean wounded;//受傷效果
    
    public int position=0;
    public List<RoadXY> roadXYList = new ArrayList<>();

 position是走到第幾步,roadXYList就是路線,動態新增6個野怪,路線別忘記新增了,3000可以理解為整條路線野怪需要走3000步才能到終點。

1.3、動態新增野怪

/**
 * 新增一個野怪
 */
private void addBlame() {
    if (countDownTimer != null) {
        return;
    }
    countDownTimer = new CountDownTimer(12000, 2000) {

        @Override
        public void onTick(long millisUntilFinished) {
            if (blameList.size() >= 6) {
                return;
            }
            BlameBean blameBean = new BlameBean();
            blameBean.setHP(100);
            blameBean.setSpeed(1);
            blameBean.setX(towerX + 200);
            blameBean.setY(0);

            List<BlameBean.RoadXY> roadXYList = blameBean.getRoadXYList();
            for (int i = 0; i < 3000; i++) {
                pathMeasure.getPosTan(i/3000f * pathMeasure.getLength(),pos,tan);
                BlameBean.RoadXY roadXY = new BlameBean.RoadXY();
                roadXY.setRoadX((int) pos[0]);
                roadXY.setRoadY((int) pos[1]);
                roadXYList.add(roadXY);
            }
            blameBean.setRoadXYList(roadXYList);

            blameList.add(blameBean);
        }

        @Override
        public void onFinish() {

        }
    };

}

1.4、新增防禦塔 ,動態建立A炮

Activity中
mBinding.create.setOnClickListener(v ->  {
    TowerView5 towerView = new TowerView5(activity);
    RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    lp.width = 700;
    lp.height = 700;
    towerView.setLayoutParams(lp);
    //一旦放置成功後呼叫
    towerView.setTowerListener(() -> {
        int raduis = 350;
        towerView.setMove(false);
        mBinding.TowerView.addTower((int)towerView.getX()+raduis,(int)towerView.getY()+raduis,raduis);
    });
    mBinding.layoutRelative.addView(towerView);
    listTower.add(towerView);
});

View中
   /**
     * 新增一個防禦塔
     */
    public void addTower(int x, int y, int raduis) {
        TowerBean towerBean = new TowerBean();
        towerBean.setTowerId(towerList.size());
        towerBean.setAttacksSpeed(500);
        towerBean.setHarm(5);
        towerBean.setX(x);
        towerBean.setY(y);
        towerBean.setRaduis(raduis);
        towerList.add(towerBean);
    }

我們新增完成需要在ondraw方法中繪製出來

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //野怪路線
        canvas.drawPath(roadPath,roadPaint);
        //皇帝
        for (int i = 0; i < blameList.size(); i++) {
            if (blameList.get(i).getX() > (towerX * 2 - 100) && blameList.get(i).getHP() > 0) {
                kingHP -= blameList.get(i).getHP();
            }
        }
        if (kingHP <= 0) {
            kingHP = 0;
            canvas.drawText("失敗", towerX, towerY, tp);
            valueAnimator.cancel();
        }
        canvas.drawText("皇帝" + kingHP, towerX * 2 - 100, 200, tp);

        //野怪移動
        for (int i = 0; i < blameList.size(); i++) {
            BlameBean blameBean = blameList.get(i);
            if (blameBean.getHP() > 0) {
                canvas.drawRect(blameBean.getX() - 40, blameBean.getY()-15, blameBean.getX() + 60, blameBean.getY()-5, towerPaint);
                canvas.drawRect(blameBean.getX() - 39, blameBean.getY() - 10, blameBean.getX() + 58 - (100 - blameBean.getHP()), blameBean.getY() - 10, hpPaint);
                bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.yeguai2);
                canvas.drawBitmap(bitmap, blameBean.getX() - 20, blameBean.getY() , tp);
            }
        }
    }

寫到這裡還沒有寫重新整理view的程式碼,帶著疑問,如何重新整理資料,如何更新野怪行走的資料,如何判斷是否在開炮射程內。

public BattlefieldView5(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //ViewGroup不跳過onDraw()方法,預設是跳過
    setWillNotDraw(false);
    ......
    valueAnimator = ValueAnimator.ofInt(0, 10);
    valueAnimator.setDuration(5000);
    valueAnimator.setInterpolator(new LinearInterpolator());
    valueAnimator.setRepeatCount(-1);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            updateParticle();
            invalidate();
        }
    });

}

解釋:我們開啟一個動畫,讓其不斷的重繪。 updateParticle方法最關鍵,記錄了野怪行動資料,開炮動畫等

private void updateParticle() {
    //野怪移動
    for (int i = 0; i < blameList.size(); i++) {
        BlameBean blameBean = blameList.get(i);
        if(blameBean.getPosition()>=3000){
            break;
        }
        int roadX = blameBean.getRoadXYList().get(blameBean.position).getRoadX();
        int roadY = blameBean.getRoadXYList().get(blameBean.position).getRoadY();
        blameBean.setPosition(blameBean.getPosition()+1);
        blameBean.setX(roadX);
        blameBean.setY(roadY);
        //野怪進入防禦塔範圍
        isAttacks(i);
        //開炮動畫
        if (blameList.get(i) != null && blameList.get(i).getMapAttacksTower().size() > 0) {
            Map<Integer, Integer> listAttacksTower = blameList.get(i).getMapAttacksTower();
            for (Integer j : listAttacksTower.keySet()) {
                shotMove(towerList.get(listAttacksTower.get(j)).getX(), towerList.get(listAttacksTower.get(j)).getY(), blameBean.getX(), blameBean.getY(), i, listAttacksTower.get(j));
            }
        }
    }
}

是否進入防禦塔範圍,這裡我們使用map來存int raduis = (int) Math.hypot(Math.abs(x), Math.abs(y));常用於勾股定理,如果在防禦塔範圍內,野怪就記錄一下id,如果在兩個防禦塔內,就把兩個防禦塔的id記錄一下,map的特性,不會有key重複。也就是不會有防禦塔重複攻擊。

public class BlameBean {
/**
*使用map的好處是唯一
* 被哪些防禦塔攻擊
* */
public Map<Integer,Integer> mapAttacksTower=new HashMap<>();
private void isAttacks(int position) {
    for (int j = 0; j < towerList.size(); j++) {
        int x = blameList.get(position).getX() - towerList.get(j).getX();
        int y = blameList.get(position).getY() - towerList.get(j).getY();
        int raduis = (int) Math.hypot(Math.abs(x), Math.abs(y));
        Map<Integer, Integer> mapAttacksTower = blameList.get(position).getMapAttacksTower();
        if (raduis < towerList.get(j).getRaduis() && blameList.get(position).getHP() > 0) {
            mapAttacksTower.put(towerList.get(j).getTowerId(), towerList.get(j).getTowerId());
            blameList.get(position).setMapAttacksTower(mapAttacksTower);
        } else {
            //移除防禦塔
            //攻擊到該野怪  塔的集合
            for (Integer key : mapAttacksTower.keySet()) {
                if (mapAttacksTower.get(key) == towerList.get(j).getTowerId()) {
                    mapAttacksTower.remove(key);
                    blameList.get(position).setMapAttacksTower(mapAttacksTower);
                    break;
                }
            }
        }
    }
}

開炮動畫,遍歷野怪可被攻擊的集合即可

//開炮動畫
if (blameList.get(i) != null && blameList.get(i).getMapAttacksTower().size() > 0) {
    Map<Integer, Integer> listAttacksTower = blameList.get(i).getMapAttacksTower();
    for (Integer j : listAttacksTower.keySet()) {
        shotMove(towerList.get(listAttacksTower.get(j)).getX(), towerList.get(listAttacksTower.get(j)).getY(), blameBean.getX(), blameBean.getY(), i, listAttacksTower.get(j));
    }
}

1.5、炮彈動畫

判斷如果可以攻擊了,就開啟一個從xy(防禦塔),移動到x2y2 (野怪)的動畫 ,動畫結束後掉血。動畫開始時不可能再次開啟,要符合防禦塔一次只能攻擊一個野怪的效果,這裡開炮動畫有點問題,就是視覺上老是打偏,有的時候炮彈慢的話,就會打在野怪身後,也沒有好的解決方式。博友有想法請留言。

//炮彈動畫
    private void shotMove(float x, float y, float x2, float y2, int blamePosition, int towerPosition) {
        if (!towerList.get(towerPosition).isAttacking()) {
            towerList.get(towerPosition).setAttacking(true);
            shotView = new ImageView(this.getContext());
            shotView.setImageDrawable(getContext().getDrawable(R.drawable.shot));
            shotView.layout(0, 0, 20, 20);
            addView(shotView);
            //開炮音效回撥
            iShotService.shot();
            translateAnimation = new TranslateAnimation(x, x2, y, y2);
            translateAnimation.setDuration(towerList.get(0).getAttacksSpeed());
            translateAnimation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    blameList.get(blamePosition).setHP(blameList.get(blamePosition).getHP() - towerList.get(towerPosition).getHarm());
                    towerList.get(towerPosition).setAttacking(false);
                    int childCount = getChildCount();
                    if (childCount > 1) {
                        removeView(getChildAt(childCount - 1));
                    }
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
            shotView.startAnimation(translateAnimation);
        }
    }

我們需要控制開始與暫停和onResume下動畫的釋放等

    public void start() {
        if (valueAnimator != null) {
            //開啟動畫
            addBlame();
//            addTower(450,450);
            valueAnimator.start();
            countDownTimer.start();
        }
    }

    public void pause() {
        if (valueAnimator != null) {
            //開啟動畫
            valueAnimator.pause();
            countDownTimer.cancel();
        }
    }

    /**
     * hasWindowFocus:true 獲得焦點,開啟動畫;false 失去焦點,停止動畫
     */
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (!hasWindowFocus) {
            if (valueAnimator != null) {
                //開啟動畫
                valueAnimator.pause();
                countDownTimer.cancel();
                countDownTimer = null;
            }
            if (countDownTimer != null) {
                countDownTimer.cancel();
                countDownTimer = null;
            }
        }
    }

 

總結:這裡加入了新的背景圖、多個防禦塔隨意擺放、一旦擺放就無法移動(後續加入拆除、升級)等功能。難點還是在野怪移動上,還有多個防禦塔攻擊互相不影響。

問題:現在的思路是重新整理一下,野怪走一步,後續如果加入減速防禦塔的話,應該怎麼走呢,多個野怪如何做到行走速度互不影響呢。

個人思路:可以正常野怪一次走5步,如果被減速防禦塔打中後,就把5步見為2步,position+5調整為position+2。這裡只是記錄學習View的過程,不要較真哦。