Java 抽象方法对象创建不好?

Java abstract method object creation is bad?

我编写游戏 多年 但我对如何编写游戏有疑问。

所以假设你有一把枪 class 用于游戏,枪可以有很多精灵、声音、外壳、射弹、参数等。那么我只需要创建另一个 class 扩展这把枪 class 并用我需要那把枪做的事情来填充我的抽象方法。

所以我做了一个抽象 class 来处理枪的所有内部代码,如果它射击,是否需要螺栓动作以及需要等待多长时间,何时播放开火声,等等 所以基本上主炮 class 为这些参数调用实际的炮 class(充满抽象方法)。

所以我的问题是,如果我在整个代码中根据需要一遍又一遍地调用这些抽象方法,那么有以下内容是不是很糟糕?

@Override
protected String getName() {
    //This should be ok
    return "Winchester";
}

@Override
protected boolean isBoltAction() {

    //This should be ok
    return false;
}

@Override
protected float getBoltActionTime() {

    //This should be ok
    return 0.5f;
}

@Override
protected Vector2 getPosOffset() {
    //problem? or not?
    return new Vector2(3,0);
}

如果我调用 getPosOffset,为简单起见,我需要将它作为一个 Vector2 对象,该对象本身应该不会太贵,但这会不会产生问题?我问是因为 afaik 即使对象本身没有改变并且是预设的,每次我调用这个方法时它都会创建一个新的对象而不需要真正需要,我可以加载一个列表并将所有内容整齐地放在那里而无需创建额外的对象但这可能会使我的摘要 class 至少对这些方法有点无用。这样做不好吗?我应该改变吗?我试着看看如果我将它加载到一个字段中与以这种方式相比需要多少纳秒,我没有看到任何重大的时间开销。

虽然游戏运行很顺利,但我不确定是我的强迫症出了问题还是这里有问题。

更新:

protected void update() {
    //get position of player
    pos.x = (float) (LevelManager.getPlayer().getPos().x);
    pos.y = (float) (LevelManager.getPlayer().getPos().y);

    //Tweener stuff to animate the recoil
    posOffset.x = recoilTweener.tick();
    
    //Fire time check
    if (timePassed >= this.salvoDelay) {
        //fire only if:
        //if its automatic then allow it
        //And
        //if not needsBoltAction and we haven't "reloaded" the boltaction, these are Firearm fields not properties
        //and
        //if mouse left button is pressed
        if (((this.automatic || !shooted) && (!needsBoltAction && !boltActionActive)) && Gdx.input.isButtonPressed(Buttons.LEFT)) {
            //prevents hold firing for non automatic weapons
            shooted = true;
            //Tells us that we should fire
            firing = true;
        } else if (!Gdx.input.isButtonPressed(Buttons.LEFT)) {
            //If the left mouse button isn't held lets reset
            shooted = false;
        }
    } else {
        //if time hasn't passed lets wait
        timePassed += Math.min(Gameloop.getFrameTime(), this.salvoDelay);
    }

    //If we can fire and it was triggered above
    if (firing) {
        //Set time for salvo, in the sense each salvo shoots x projectiles within a set amount of time in between
        timePassed += Math.min(Gameloop.getFrameTime(), this.salvoDelay + this.shotsPerSalvo * this.shotDelay);
        //if we have shots to fire
        //and
        //if timepassed is bigger than the current shot * shotDelay
        if (this.shotsPerSalvo > shotsFired && timePassed - this.salvoDelay >= shotsFired * this.shotDelay) {
            if (this.shotDelay == 0) {
                //Create projectile and set it up
                createProjectile(this.shotsPerSalvo);
                //Since we have 0 shotDelay we set this to false and reset the gun
                firing = false;
                shotsFired = 0;
                timePassed = 0;
                //"Ask" properties if we need boltAction
                needsBoltAction = this.boltAction;
            } else {
                //Create each individual shot according to the time
                createProjectile(1);
                //Increase shotsFired for calculations
                shotsFired++;
            }
        //If shotsFired reached the shotsPerSalvo, again each salvo has x shots to be fired in an instant (example shotguns, glocks in burst mode)
        } else if (shotsFired >= this.shotsPerSalvo) {
            //Reset weapon
            firing = false;
            shotsFired = 0;
            timePassed = 0;
            //"Ask" properties if we need boltAction
            needsBoltAction = this.boltAction;
        }
    }

    //BoltAction logic
    //If needsBoltAction was triggered and we press R "reload" and wait (boltActionTime)
    if (needsBoltAction && Gdx.input.isKeyJustPressed(Keys.R)) {
        //Doing boltaction
        boltActionActive = true;
        //We dont need to press R again
        needsBoltAction = false;
    }

    //We are doing the boltAction and not firing the gun
    if (boltActionActive && !firing) {
        //Add time
        //Remember that timePassed was reset when all shots were fired on a salvo, in any situation
        timePassed += Math.min(Gameloop.getFrameTime(), this.boltActionTime);
        //If time was enough
        if (timePassed >= this.boltActionTime) {
            //We can shoot again since boltAction is done
            boltActionActive = false;
            timePassed = 0;
        }
    }

}

private void createProjectile(int amount) {

    if (amount <= 0) {
        Console.write(new Color(1,0,0,1),"Error: Projectile amount is invalid!");
    }

    if (getFireSoundFile() != null) {
        getFireSoundFile().play();
    }

    if (this.casing != null) {
        for (int i = 0; i < this.casingsPerSalvo; i++) {
            ParticleManager.add(createCasing());
        }
    }

    recoilTweener.reset();

    for (int i = 0; i < amount; i++) {
        Projectile p = createProjectile();
        p.setup(LevelManager.getPlayer().getPos().x + (float) (Math.cos(Math.toRadians(WeaponManager.angle)) * this.shotStartingPosOffset.x + Math.sin(Math.toRadians(WeaponManager.angle)) * this.shotStartingPosOffset.y), LevelManager.getPlayer().getPos().y + (float) (Math.sin(Math.toRadians(WeaponManager.angle)) * this.shotStartingPosOffset.x + Math.cos(Math.toRadians(WeaponManager.angle)) * this.shotStartingPosOffset.y), WeaponManager.angle + Utils.getRandomFloat(this.spread,true));
        WeaponManager.addProjectile(p);
    }
}   
private Casing createCasing() {
    return new Casing(this.casing, this.pos);
}

private Projectile createProjectile() {
    return new Projectile(this.projectile);
}

目前我的属性是这样读的:

    protected Firearm() {       
    loadParameters();
    //other stuff
}

/**
 * Loads all guns parameters from the abstract methods
 * This is to only load these methods only once
 */
private void loadParameters() {
    this.casing = getCasing();
    this.magazineDrop = getMagazineDrop();
    this.casingsPerSalvo = getCasingsPerSalvo();
    this.unlimitedAmmo = hasUnlimitedAmmo();
    this.projectile = getProjectile();  
    this.projectileDamage = getProjectileDamage();
    this.spread = getSpread();
    this.shotsPerSalvo = getShotsPerSalvo();
    this.salvoDelay = getSalvoDelay();
    this.shotDelay = getShotDelay();
    this.automatic = isAutomatic();
    this.fireSound = getFireSound();
    this.reloadSound = getReloadSound();
    this.name = getName();
    this.boltAction = isBoltAction();
    this.boltActionTime = getBoltActionTime();
    this.shotStartingPosOffset = getShotStartingPosOffset();
    this.recoilOffset = getRecoilOffset();      
}

/**
 * Gets bullet casing sprite
 */
protected abstract Casing getCasing();
protected Casing casing;

/**
 * Gets magazine object
 */
protected abstract Magazine getMagazineDrop();
protected Magazine magazineDrop;

/**
 * Number of casing drops per salvo
 */
protected abstract int getCasingsPerSalvo();
protected int casingsPerSalvo;

/**
 * If the weapon has unlimited ammo
 */
protected abstract boolean hasUnlimitedAmmo();
protected boolean unlimitedAmmo;

/**
 * Projectile texture path
 */
protected abstract Projectile getProjectile();
protected Projectile projectile;

/**
 * Projectile Damage
 */
protected abstract float getProjectileDamage();
protected float projectileDamage;

/**
 * Angle spread, angle added to each shot
 */
protected abstract float getSpread();
protected float spread;

/**
 * Shots fired per salvo
 */
protected abstract int getShotsPerSalvo();
protected int shotsPerSalvo;

/**
 * Not to be confused with getShotDelay
 * This is the delay per salvo (each salvo will fire a number of shots)
 */
protected abstract float getSalvoDelay();
protected float salvoDelay;

/**
 * Delay per shot on a salvo
 */
protected abstract float getShotDelay();
protected float shotDelay;

/**
 * If true then the pistol is automatic, if false the pistol is semi-automatic
 */
protected abstract boolean isAutomatic();
protected boolean automatic;

/**
 * If true then the pistol is automatic, if false the pistol is semi-automatic
 * Note: this should only return the name of the file+extension, the file will be looked up in the internal folder "sounds/sfx/weapons/fire"
 */
protected abstract String getFireSound();
protected String fireSound;

/**
 * If true then the pistol is automatic, if false the pistol is semi-automatic
 * Note: this should only return the name of the file+extension, the file will be looked up in the internal folder "sounds/sfx/weapons/fire"
 */
protected abstract String getReloadSound();
protected String reloadSound;

/**
 * Weapon's name
 */
protected abstract String getName();
protected String name;

/**
 * If true then player will need to press R to reload
 */
protected abstract boolean isBoltAction();
protected boolean boltAction;

/**
 * Time of bolt action
 */
protected abstract float getBoltActionTime();
protected float boltActionTime;

/**
 * Firearm's bullet starting position offset
 * Will automatically rotate angle
 */
protected abstract Vector2 getShotStartingPosOffset();
protected Vector2 shotStartingPosOffset;

/**
 * Firearm's recoil in units to be subtracted to the weapons default offset position
 * Will make the firearm go backwards and fowards with a tweener according to the units
 * I am putting a vector2 just incase I need the firearm to recoil in the y vector but I atm dont see any use
 */
protected abstract Vector2 getRecoilOffset();
protected Vector2 recoilOffset;

以防万一您需要以下属性:

    public class Winchester extends Firearm {

public Winchester() {
    super();
}

@Override
public String getSprite() {

    return "winchesterwh.png";
}

@Override
public String getSpriteDropped() {

    return null;
}

@Override
protected Casing getCasing() {

    return new Casing("sprites/weapons/casing/smallcasing.png", new Vector2(0,0));
}

@Override
protected Magazine getMagazineDrop() {

    return null;
}

@Override
protected int getCasingsPerSalvo() {

    return 1;
}

@Override
protected boolean hasUnlimitedAmmo() {

    return false;
}

@Override
protected Projectile getProjectile() {

    return new Projectile("sprites/weapons/projectiles/bullet.png", new Vector2(2,20), 50f, 0f, getProjectileDamage());
}

@Override
protected float getProjectileDamage() {
    return 10f;
}

@Override
protected float getSpread() {

    return 1f;
}

@Override
protected int getShotsPerSalvo() {

    return 5;
}

@Override
protected float getSalvoDelay() {

    return 0.25f;
}

@Override
protected float getShotDelay() {

    return 0.25f;
}

@Override
protected boolean isAutomatic() {

    return false;
}

@Override
protected String getFireSound() {

    return "sniperShot.ogg";
}

@Override
protected String getReloadSound() {

    return null;
}

@Override
protected String getName() {

    return "Winchester";
}

@Override
protected boolean isBoltAction() {

    return false;
}

@Override
protected float getBoltActionTime() {

    return 0.5f;
}

@Override
protected Vector2 getPosOffset() {
    return new Vector2(3,0);
}

@Override
protected Vector2 getShotStartingPosOffset() {
    return new Vector2(0,5);
}

@Override
public float getDamage() {
    return 0;
}
@Override
public Vector2 getRecoilOffset() {
    return new Vector2(3,0);
}  } // cant seem to get this bracket to behave

我认为如果我使用 XML 代替加载属性

,每个人都会高兴的解决方案

此设计与 OOP 的主要原则之一相矛盾 - Tell-Don't-Ask。 IE。而不是像这样的代码:

abstract class Gun {
    
    public void fire() {
        if (isBolt()) {
           // do bolt specifics
        } else if (isArrow()) {
           // do arrow specifics
        } else if (...)
    }
}

你需要这样的代码:

abstract class Gun {
    abstract void fire();
}

class Bow extends Gun {
    public void fire() {
        // fire an arrow
    }
}

class CrossBow extends Gun {
    public void fire() {
        // fire a bolt
    }
}

您在问题中定义的主要动机是 GoF Template Method pattern,但请注意,它定义了 整个处理过程 的框架,不应包含 if (type == "laser") 混入。

通常常见的超级 class 应该只有属性 (fields/methods) 对于所有子 class 是真正常见的。如果你在那里,比如说,boltActionTime 只被一个子 class 使用,那是错误的(当然 base class 应该知道任何关于 sub-classes).

如果我的设计正确,你的基础 class GunBase 如下:

public abstract class GunBase
{
    public abstract String getImageFileName();
}

和这样的实现:

public final class Winchester extends GunBase
{
    @Override
    public final String getImageFileName() { return "Winchester.png"; }
}

如果 GunBase 真的只包含那些 getter 方法,它根本不应该是 class,而是 界面.

如果 GunBase 确实提供了仅由那些 getter 的 return 值参数化的功能,那么根本不应该覆盖那些 getter;相反,基础 class 应该获得相应值的属性:

public abstract class GunBase
{
    private final String m_ImageFileName;

    protected GunBase( final String imageFileName )
    {
        m_ImageFileName = imageFileName;
    }

    public final String getImageFileName() { return m_ImageFileName; }
}

派生的 class 将如下所示:

public final class Winchester extends GunBase
{
    public Winchester() 
    { 
        super( "Winchester.png" ); 
    }
}

承认,在这种情况下,受保护的构造函数的参数列表可能会变得很长,但这可以通过使用包含所有值的映射而不是离散参数来解决。

现在只有那些描述每支枪不同行为的方法仍然是抽象的(真正不同,不仅仅是参数值不同)。

但是如果一把枪实际上只是值的集合,你应该想想为什么你把枪设计成一棵 class 树,而不是 class 的集合Gun 其中每个实例描述一种武器类型。

我想知道您是否混淆了 类 和实例。假设您有一把 AK-47。无论您有多少支 AK-47,它们的外观和行为都将相同,除了某些特殊用途,例如遮阳板或不同的枪托。

这意味着它们本质上是常量。毕竟,AK-47 不会是双管猎枪。也不会射箭

你需要做的是看看你的不同枪之间在逻辑方面有什么本质上的不同。因为剩下的都是属性。如果只有属性,则不需要 sub类.

如果您需要不止一支 AK-47,您只需要一个列表,所有这些都指向同一个 AK-47 实例。

即使你有不同的行为,从某种意义上说,它需要不同的代码,在枪支之间,你也可以将它们归为一组:散弹枪、燧发枪、全自动等。在这种情况下,你只需要 类 不同的行为。

宠物也一样。你可以养狗、猫、仓鼠、鱼等等。但无论你养的是英国短毛猫、波斯猫、孟加拉猫还是普通的流浪猫,它们都会发出“喵喵”的叫声,而且耳朵尖尖的。您不需要为每种猫都实现“喵喵叫”。

以下是您根据我的建议实施的迷你版:

public class Test {
    public static void main(String[] arg) throws Exception{
        Firearm gun = new Winchester();
        gun.fire();
        
        gun = new HenryRifle();
        gun.fire();
    }

}

class Winchester extends Firearm {
    //as you are hard coding the values, 
    //the property belongs to the class Winchester 
    private static Vector2 wVec = new Vector2(1,2);
    
    public Winchester() {
        //if wVec was not static then use
        //super.posOffset = wVec;
    }
    
    @Override
    protected Vector2 getPosOffset() {
        return wVec;
    }
}

class HenryRifle extends Firearm {
    private static Vector2 hVec = new Vector2(3,0);
    @Override
    protected Vector2 getPosOffset() {
        return hVec;
    }
}

abstract class Firearm {
    Vector2 posOffset;
    protected abstract Vector2 getPosOffset();
    protected Firearm() {
        this.posOffset = getPosOffset();
    }

    //all sub classes will inherit fire method 
    //no need to override it everywhere
    public void fire() {
        System.out.println("bang bang at " + posOffset);
    }
}

record Vector2(int x, int y) {}

这输出:

bang bang at Vector2[x=1, y=2]
bang bang at Vector2[x=3, y=0]

我认为您需要将逻辑与太多数据分开: 创建类似 **GunProps.getProperty("manchester", "posOffset")** 的内容以在 Firearm class 中提供逻辑。如果枪有不同的行为,则在 sub class 中覆盖该方法。 对于调用一种方法并从 Map of Maps 中检索数据,性能影响不大。

Rumtime 效率和 abstraction/design 是两个不同的东西。就像你说的:

I could load a list and put everything there neatly without creating additional objects...

这带来了运行效率,但它不会使抽象 class 变得无用,因为还可以有多种策略。

比如子弹打出去的时候,弹道是怎么计算的?你可以想出很多不同的策略来让它变得更有趣,而且随着时间的推移你会创造出越来越多的东西。如果您将这些策略混合在一种方法中,适用于所有类型的枪支,将很难扩展。最好的选择是将它们分开,创建抽象。