Oggetti modulari con implementazioni in conflitto

6

Sto lavorando a un framework di gioco che consenta agli utenti di utilizzare più modifiche / plug-in per aggiungere funzionalità aggiuntive al gioco.

Per il resto del post, utilizzerò le seguenti definizioni:

mod : codice che richiede modifiche sia al client che al server. Ad esempio, aggiungendo un nuovo pacchetto al protocollo.

plugin : codice che modifica la funzionalità esistente senza richiedere una modifica sull'altro lato. Ad esempio, aggiungendo un'interfaccia di gestione remota al server o consentendo al client controlli più dettagliati rispetto alla qualità audio e audio.

Questa domanda si riferisce a entrambi questi aspetti, tuttavia mi concentrerò su mods in quanto sembrano darmi i maggiori problemi.

Diciamo che fornisco una classe giocatore predefinita chiamata Player che modella il personaggio di un giocatore. Diciamo che, per questo particolare server, l'operatore del server vuole includere il 2% dimods,% diFoo e% diBar, entrambi scritti da persone diverse.

Sia Foo che Bar devono aggiungere funzionalità aggiuntive alla classe Player . Foo ha bisogno di un metodo doFoo e Bar ha bisogno di un metodo doBar . Entrambe le implementazioni effettive sono incluse in qualche modo dagli sviluppatori di mod (probabilmente in una classe diversa).

Una soluzione che ho preso in considerazione è l'utilizzo di Reflection e la modifica della classe in fase di runtime, estendendo specifiche interfacce mod. Qualcosa come

public interface FooPlayer {
    void doFoo();
}

public interface BarPlayer {
    void doBar();
}

public class FooPlayerImplementation implements FooPlayer {
    public void doFoo() {
        // ...
    }
}

public class BarPlayerImplementation implements BarPlayer {
    public void doBar() {
        // ...
    }
}

public class FooLoader {

    public void onLoad() {
        SomeManager.findPlayerClass()
                   .implementInterface(FooPlayer.class, FooPlayerImplementation.class);
    }
}

public class BarLoader {
    public void onLoad() {
        SomeManager.findPlayerClass()
                   .implementInterface(BarPlayer.class, BarPlayerImplementation.class);
    }
}

Dove in fase di esecuzione, inietterebbe la classe Player con le implementazioni da Foo e Bar , e renderà implementare FooPlayer e BarPlayer . Ma poi abbiamo dei problemi in cui due diverse mod aggiungono la stessa firma del metodo nella loro interfaccia (che potrebbe essere risolta anteponendo il nome della classe al metodo, ma poi scendiamo in una buia tana molto scura).

Un'altra cosa a cui ho pensato brevemente è stata solo un'eccitazione folle, quindi gli sviluppatori non hanno bisogno di fare qualcosa del genere, ma questo apre una altrettanto worm, oltre a non fornire la quantità di controllo di cui uno sviluppatore potrebbe aver bisogno.

Ma vorrei REALMENTE come evitare di riflettere se possibile.

Ho visto AspectJ e la programmazione orientata all'aspetto, e sembra che possa funzionare, ma non ho alcuna esperienza da raccontare.

Le domande che ho sono:

  1. C'è un modo per ottenere questo tipo di funzionalità in modo OOP?
  2. C'è un modo per farlo con la libreria Java SE standard?
  3. Potrebbe farlo questo AOP?

Considerazioni : se uno di questi è sbagliato o mal guidato, faccelo sapere.

  1. Non penso che l'ereditarietà funzioni perché potrebbero esserci più mod che vogliono estendere la funzionalità di Player , e tutto Player s sarebbe soggetto a tutto il mods . Non ci potrebbe essere un FooPlayer e un FooBarPlayer allo stesso tempo. Se n mod estende la funzionalità Player, che il Player deve avere componenti n .
  2. Il pattern decoratore non funzionerà davvero, penso perché l'intervento manuale sarebbe richiesto dall'operatore (che può o non può sapere qualcosa sulla programmazione) per tessere insieme le implementazioni.

Qualcosa di simile

public class FooBarStaminaMagickaPlayer implements Player, Foo, Bar, Stamina, Magicka
  1. Se possibile, vorrei evitare che ogni sviluppatore mod abbia una mappatura all'interno del proprio codice per tenere traccia delle cose.

Ad esempio, qualcosa di simile

public class StaminaMapper {
    private final Map<Player, Stamina> playerStaminaMap;
}
    
posta Zymus 07.05.2016 - 04:41
fonte

5 risposte

1

C'è un'altra soluzione. Può rallentare il codice (non così male come riflessione però).

Tutto ha un'interfaccia. Le classi eseguiranno solo la composizione delle funzioni: pipe restituisce i parametri ai ritorni, ecc. Tutti i prameters e le variabili di istanza sono interfacce.

Una mod o un plugin è solo un pacchetto di implementazioni per le interfacce. Crea un mod chiamato default. Metti questo nel suo pacchetto. In questo modo, il resto è un codice API.

Il caricamento del mod deve essere fatto. Ora un contenitore può iniettare le classi e cercare le cartelle mod.

Può esserci un elenco di tutte le modifiche in un file, in ordine di priorità. Con un conflitto, la prima mod elencata sarà quella usata nel contenitore.

Raccomando un framework di iniezione delle dipendenze per questo. Vorrei anche raccomandare l'uso di clausole gaurd. con messaggi nell'espezione per intervalli validi di parametri. Solo perché glitch e exploit sarebbero fatti.

    
risposta data 10.05.2016 - 20:10
fonte
1

Probabilmente ti senti come se tutte le tue opzioni ti portassero giù in una "buca molto buia" perché sei già in una tana di coniglio, e la soluzione corretta è uscire dalla buca e prendere in considerazione una nuova direzione .

Ecco un link su quanti giochi sono sviluppati che potrebbero indirizzare le tue preoccupazioni indirettamente ma in modo più sostanziale.

link

Questa citazione penso che indirizzi il tuo roadblock esatto:

An ECS follows the Composition over inheritance principle that allows greater flexibility in defining entities where every object in a game's scene is an entity (e.g. enemies, bullets, vehicles, etc.). Every Entity consists of one or more components which add additional behavior or functionality. Therefore the behavior of an entity can be changed at runtime by adding or removing components. This eliminates the ambiguity problems of deep and wide inheritance hierarchies that are difficult to understand, maintain and extend.

e

link

Ok questo è il mio tentativo di darti un esempio di come risolvere il problema usando ECS. Questo può essere notevolmente migliorato. Lo scopo è mostrare il design, non i dettagli su come farlo funzionare in modo ottimale.

abstract class Component
{
    public string ComponentType { get; } // implements some identifier which modders can generate/register for new components
}

class Position : Component { }
class Velocity : Component { }
class Health : Component { }

abstract class Entity
{
    public List<Component> Components;
    public abstract T GetComponentOfType<T>();
    public abstract Component GetComponentById(string Id);
}

class MovementSystem
{
    public static void Execute(Position p, Velocity v)
    {
        // do stuff with p and v
    }
}


// When it comes time to add new components, all you do is add the modder's new component, system, registration, and any other mechanism for incrementing the game loop.

class Stamina : Component { }

class StaminaSystem
{
    void Execute(Position p, Velocity v, Stamina s)
    {
        // do stuff with p and v and s
    }
}

class StaminaSystemLoop
{
    bool IsEntityEquipedWithCorrectComponentsForThisSystem(Entity e);

    void Increment(Entity e)
    {
         StaminaSystem.Execute(e.GetComponentOfType<Position>(), e.GetComponentOfType<Velocity>(), e.GetComponentOfType<Stamina>());
    }
}



class GameLoop
{
    void Increment(List<Entity> gameEntities)
    {
        foreach(var e in gameEntities)
        {
             // Detect if entity is a match for any given system.
             // Execute the system with the components using GetComponentOfType.
             //
             // The installation process for the mod will need to add the detection logic and the execution logic.
             // So this is where you have to get creative about it. E.g.:
             if(StaminaSystemLoop.IsEntityEquipedWithCorrectComponentsForThisSystem(e))
                  StaminaSystemLoop.Increment(e);
        }
    }
}
    
risposta data 10.05.2016 - 15:05
fonte
0

Risponderò solo alla tua prima domanda poiché non ho abbastanza esperienza per pronunciarmi su AOP.

Il mio modo preferito di estendere una parte di codice completa (capire non astratta) è utilizzare il schema di decorazione .

Diciamo che questo è quello che hai definito un giocatore minimo per il tuo framework. Il punto più importante è che ogni metodo pubblico in PlayerImpl deve essere ereditato da Player (vedi sotto perché):

public interface Player {
    void doAction();
}

public final class PlayerImpl implements Player {
    @Override
    public void doAction() {
        //do action
    }
}

È quindi possibile che gli sviluppatori di mods possano tranquillamente (senza alterare l'esistente) aggiungere un comportamento a un giocatore usando il modello decoratore:

public interface Foo {
    void foo();
}

public final class FooPlayer implements Player, Foo {
    private final Player origin;

    public FooPlayer(Player origin) {
        this.origin = origin;
    }

    @Override
    public void doAction() {
        origin.doAction(); //or define totally new behaviour when needed (typically for a plugin)
    }

    @Override
    public void foo() {
        // put special mod behaviour
    }
}

E poi in uso, le istanze di FooPlayer sono sempre utilizzabili dal tuo framework (perché ereditano da Player ) e lo sviluppatore mod può anche usare il comportamento specifico nel suo codice quando necessario:

public static void main(String[] args) {
    FooPlayer p1 = new FooPlayer(MyGame.provideNewPlayer());
    MyGame.registerNewPlayer(p1); //still compatible with what you provide altough holding a reference of FooPlayer
    p1.doAction();
    p1.foo();
}
    
risposta data 10.05.2016 - 10:55
fonte
0

Non penso che nessuna forma di ereditarietà funzionerà in modo affidabile con questo. Come hai identificato, avrai dei conflitti e creerai un insieme fragile e fragile di classi. Se cambi la tua classe base (per qualsiasi motivo), anche tutti i tuoi clienti dovranno cambiare.

AOP potrebbe funzionare per te, ma penso che non sarà intuitivo, e non credo che sarà particolarmente resiliente di fronte alle modifiche al codice.

La mia soluzione preferita in questo tipo di scenario è seguire la tua idea di osservatore. I client A e B possono entrambi registrare i listener e quindi eseguire il codice in base alle modifiche registrate dalla classe del server. Probabilmente scoprirai di dover pubblicare eventi per la maggior parte (tutti?) Delle modifiche allo stato e forse rendere disponibili altre entità nel tuo sistema (tramite un'API supportata) per i tuoi clienti. Allo stato attuale, sembra che le tue esigenze siano piuttosto vaghe, e per il momento farei qualcosa di semplice e vedere cosa succede.

    
risposta data 10.05.2016 - 11:13
fonte
-1

Per la fusione, crea una classe chiamata FooBlender. Dove Foo è l'interfaccia. Avrà una lista di Foo. Per ogni chiamata al metodo, ottieni l'elenco di ciò che restituiscono le altre classi. Quindi restituire la media o la miscela di tale elenco.

    
risposta data 11.05.2016 - 02:09
fonte

Leggi altre domande sui tag