2

自定义View6 -塔防小游戏:第三篇防御塔随意放置+多组野怪 - TMusketeer

 1 year ago
source link: https://www.cnblogs.com/cmusketeer/p/16671760.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

第一篇:一个防御塔+多个野怪(简易版)
第二篇:防御塔随意放置
第三篇:防御塔随意放置+多组野怪

  1、动态addView防御塔

  2、防御塔放置后不可以移动

  3、弯曲道路

  4、素材替换

第四篇:多波野怪

第五篇:杀死野怪获得金币

第六篇:防御塔可升级,增强攻击力,增大射程

1258190-20221004193740248-668738245.gif

描述:防御塔可以放置多个,每一个都是独立的,他们的攻击互不影响(防御塔随意拖动在第二篇),这里用到的知识是,自定义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(下面有解)。

1258190-20221004194924696-1531596112.png

1258190-20221004194947035-1786796310.png

1258190-20221004195017552-101133234.png

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);
}
1258190-20221004200400276-162260793.jpg

大体路线已经出来了,我们需要获取这条线上的坐标点,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炮

1258190-20221004204542022-385551956.jpg
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的过程,不要较真哦。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK