Προς το περιεχόμενο

Προτεινόμενες αναρτήσεις

Δημοσ.

Καλησπέρα παίδες B)

 

Πρόσφατα ξεκίνησα να ασχολούμαι με ένα Dungeon text game σε C++ προκειμένου να εξασκηθώ λίγο με αντικειμενοστραφής σχεδίαση / υλοποίηση.

 

Να σας δώσω λοιπόν μια μικρή περιγραφή για το παιχνίδι.

 

Έχουμε ένα μεγάλο λαβύρινθο ο οποίος περιέχει πολλά τέρατα/αντικείμενα και σκοπός του παίχτη είναι να εξολοθρεύσει όλα τα τέρατα. Ο παίχτης συμπολίζεται ως Η μέσα στον λαβύρινθο. Χρησιμοποιώ έναν 2D char array για την αναπαράσταση του λαβυρίνθου. Με λίγα λόγια το παιχνίδι έχει ένα κύκλο εκτέλεσης.

player->executeTurn

monster->executeTurn

O παίκτης μπορεί είτε να μετακινιθεί στον λαβύρινθο είτε να επιτεθεί σε κάποιο τέρας που είναι κοντά. Αρχικά θέλω να το φτιάξω να έχει πολυ βασικά πράγματα και αργότερα θα βάλω πιο ψαγμένα χαρακτηριστικά :P

 

Έχω λοιπόν 2 θεματάκια :D

 

i)

Έχω δημιουργήσει τις εξής κλάσεις : Screen, Keypad, Dungeon, Player

Screen - Περιέχει μια συνάρτηση για εξόδο σε οθόνη.

Keypad - Περιέχει μια συνάρτηση για είσοδο χρήστη.

 

Η κλάση Dungeon περίεχει τον λαβύρινθο και μια συνάρτηση run οπου εκεί θα ξεκινάει η εκτέλεση του παιχνιδιού. Επίσης έχω δηλώσει ως μέλη αντικείμενα Screen, Keypad, Player.

Να σας πω λίγο πως το είχα σκεφτεί αρχικά

 

while ( true )
{
     // Execute Player Turn
     player.execute();
}

 

Όπου η execute είναι μια συνάρτηση η οποία θα ρωτάει τον παίκτη τι θέλει να κάνει( move / attack ) και ανάλογα θα εκτελεί μια άλλη συνάρτηση move / attack. Στην move π.χ θα επιλέγει κατεύθυνση (Ανατολικά/δυτικά κλπ) και έπειτα ΗΘΕΛΑ να προσπελαύνει ένα αντικείμενο Dungeon.

 

Ωστόσο αν δηλώσω στην κλάση Player ένα μέλος αντικείμενο Dungeon θα μου βγάλει error στο compile. Νομίζω οτι αυτό το πρόβλημα λέγεται κυκλική συμπερίληψη. Δηλαδή και η κλάση Dungeon.h περιέχει μια δήλωση #include "Player.h" και η κλάση Player περιέχει δήλωση #include "Dungeon.h". Ελπίζω να καταλάβατε τι εννοώ.

Πραγματικα το ξέρω ότι σας έχω ήδη κουράσει αρκετά αλλά κάντε μια προσπάθεια να το διαβάσετε όλο και να με βοηθήσετε, αν μπορείτε :)

 

Με λίγα λόγια, το σωστό OOD για αυτό το παίχνιδι έτσι δεν θα έπρεπε να ήταν? Η κλάση Player δεν θα έπρεπε να περιέχει τις συναρτήσεις με τις οποίες ο παίκτης θα κινείται / επιτίθεται?


Η λύση λοιπόν που βρήκα εγώ, δεν ξέρω αν συμβαδίζει με την αντικειμενοστραφής υλοποίηση, είναι η εξής : Να δημιουργήσω στην κλάση Dungeon ΌΛΕΣ τις απαραίτητες συναρτήσεις για να κινείται / επιτίθεται ο παίκτης και αντίστοιχα τα τερατάκια :D

Είναι σωστό κάτι τέτοιο?

 

ii )

Επίσης έχω ένα πρόβλημα σχετικά με την αρχικοποίηση του λαβυρίνθου. Αρχικά ήθελα να συμπεριλάβω στα private μέλη ενα 2D char array και επείτα να τον κάνω initialize στον constructor. Κάτι τέτοιο δεν γινόταν γιατι μου εμφάνιζε σφάλμα και κατέληξα να τον δηλώσω και να τον αρχικοποιήσω σαν global variable στην κλάση Dungeon.cpp

Η ερώτηση μου είναι ποιος είναι ο σωστός τρόπος να δημιουργήσω τον λαβύρινθο σε μια αντικειμενοστραφής υλοποίηση...?

 

 

Το ρεζουμε είναι ότι ψάχνω κάποιες "οδηγίες" έτσι ώστε να με βάλετε στον σωστό δρόμο για να δημιουργήσω ένα σωστά δομεμημένο αντικειμενοστραφές game.

Σας ευχαριστώ όλους για το κουράγιο σας :-D

 

Υ.Γ : Αν δεν εχω εξηγήσει τα προβλήματα που αντιμετωπίζω καλά κάντε μου ενα reply και θα προσπαθήσω όσο μπορώ να γίνω πιο περιγραφικός.

Δημοσ.

Καθαρά απο άποψη OOP το λογικό θα ήταν να δηλώσεις τον Player/Monsters ως μέλη της κλάσης Dungeon. O player μπορεί να έχει κάποιες μεταβλητές οι οποίες να αφορούν το "Location" αν θέλεις, αλλά δεν είναι απαραίτητο να σχετίζεται με τη κλάση Dungeon. H κλάση Dungeon θα διαβάζει αυτές τις μεταβλητές και θα τις κάνει "translate" έτσι όπως θέλεις.

Δημοσ.

Καθαρά απο άποψη OOP το λογικό θα ήταν να δηλώσεις τον Player/Monsters ως μέλη της κλάσης Dungeon. O player μπορεί να έχει κάποιες μεταβλητές οι οποίες να αφορούν το "Location" αν θέλεις, αλλά δεν είναι απαραίτητο να σχετίζεται με τη κλάση Dungeon. H κλάση Dungeon θα διαβάζει αυτές τις μεταβλητές και θα τις κάνει "translate" έτσι όπως θέλεις.

 

Αυτό έχω κάνει, έχω δηλώσει 2 μεταβλητές στην Player

positionX, positionY και εχω φτιάξει setters/getters για αυτές τις 2 μεταβλητές.

 

Γιατι η κλαση player ειναι μελος της κλασης dungeon;

 

Το έχω σκεφτεί με την λογική ότι : H Dungeon είναι η κλάση η οποία θα περίεχει την "λογική" του παιχνιδιού και απο αυτήν θα ξεκινάει η εκτέλεση αφου περιέχει την συνάρτηση run(). Προκειμένου να χειρίζομαι και να ανανεώνω τη θέση του παίκτη έχω δηλώσει στην Dungeon να έχει ενα αντικείμενο μέλος Player.

 

Σας παραθέτω τον κώδικα σε spoiler.

 

 

Dungeon.h

 

#ifndef DUNGEON_H
#define DUNGEON_H

#include "Keypad.h"
#include "Player.h"
#include "Screen.h"

class Dungeon
{
public:
    Dungeon();
    void run();

private:
    Keypad keypad;
    Screen screen;
    Player player;

    // Private helpful functions
    void executePlayerTurn();
    void doPlayerMove();
    void printMap();
};

#endif

 

Dungeon.cpp

 

#include <iostream>
using namespace std;

#include "Dungeon.h"
 
// Dilwsi 2d char array. Den ton evala giati einai terastio 
Dungeon :: Dungeon()
{

}

void Dungeon :: run()
{
    
    // Thetw mia tixea thesi mesa ston lavirintho
    player.setPositionX( 9 );
    player.setPositionY( 5 );
    map[ player.getPositionX() ][ player.getPositionY() ] = 'h';
    printMap();

    // Game Loop
    while ( true )
    {
        // Execute Player Turn
        executePlayerTurn();
        break;
    }

}
        
void Dungeon :: executePlayerTurn()
{
    int playerMove;

    screen.displayMessage( "Choose one of the following\n" );
    screen.displayMessage( "1. Move through the dungeon\n2. Attack a monster\n3. Pick up an object\n" );
    screen.displayMessage( "Choice = " );
    playerMove = keypad.getInput();
    

    switch ( playerMove )
    {
        case 1:
            doPlayerMove();
            break;
    }
}

void Dungeon :: doPlayerMove()
{
    int direction;

    // Delete the old position
    map[ player.getPositionX() ][ player.getPositionY() ] = ' ';

    // Check if the direction is right
    while( true )
    {
        screen.displayMessage( "In Which direction u wanna go?\n" );
        screen.displayMessage( "1. South\n2. North \n3. East\n4. West\n" );
        direction = keypad.getInput();

        if ( direction == 1 )
        {
            if ( map[ player.getPositionX() + 1 ][ player.getPositionY() ] == '#' )
            {
                screen.displayMessage( "There's a wall in that direction. Choose another one." );
            }
            else
            {
                player.setPositionX( player.getPositionX() + 1 );
                break;
            }
        }
        else if ( direction == 2 )
        {
            if ( map[ player.getPositionX() - 1 ][ player.getPositionY() ] == '#' )
            {
                screen.displayMessage( "There's a wall in that direction. Choose another one." );
            }
            else
            {
                player.setPositionX( player.getPositionX() - 1 );
                break;
            }
        }
        else if ( direction == 3 )
        {
            if ( map[ player.getPositionX() ][ player.getPositionY() + 1 ] == '#' )
            {
                screen.displayMessage( "There's a wall in that direction. Choose another one." );
            }
            else
            {
                player.setPositionY( player.getPositionY() + 1 );
                break;
            }
        }
        else if ( direction == 4 )
        {
            if ( map[ player.getPositionX() ][ player.getPositionY() - 1 ] == '#' )
            {
                screen.displayMessage( "There's a wall in that direction. Choose another one." );
            }
            else
            {
                player.setPositionY( player.getPositionY() - 1 );
                break;
            }
        }
        else
        {
            screen.displayMessage( "You have not entered a valid direction. Please choose a correct one" );
        }
    }

    // Add the new player position inside the map
    map[ player.getPositionX() ][ player.getPositionY() ] = 'h';
}

void Dungeon :: printMap()
{
    for ( int i = 0; i <= 22; i++ )
    {
        for ( int j = 0; j <= 78; j++ )
        {
            cout << map[ i ][ j ];
        }
        cout << endl;
    }
}

 

 

 

Ουσιαστικά αυτό που έχω υλοιποίησει μέχρι στιγμής είναι η κίνηση του παίκτη στο λαβύρινθο.

Είναι σωστός αυτός ο τρόπος υλοποίησης του παιχνίδιου? Αν το συνεχίσω με αυτή τη νοοτροπία, θα δημιουργήσω αντιστοίχα μια συνάρτηση monsterTurn() μέσα στην Dungeon όπου εκει το τέρας αν υπάρχει κοντά ο παίκτης θα του επιτίθεται.

Αρχικά όμως επείδη σκεφτόμουν να βάλω πάνω απο ένα είδος Monster ήθελα να χρησιμοποιήσω πολυμορφισμό. Δηλαδη να δημιουργήσω μια συλλογή απο τέρατα στην κλάση Dungeon και μέσα σε ενα Loop

να κάνω Monster *m, m -> executeTurn();

δηλαδή ο *m να δείχνει προς διαφορετικά είδη τεράτων κάθε φορά. Σε αυτή τη περίπτωση η συνάρτηση executeTurn δεν θα πρέπει να βρίσκεται ως virtual στην κλάση Monster και κάθε υποκλάση της να την υλοποιεί διαφορετικά? ( π.χ ranged monster / melee monster )

 

Πρέπει να σας έχω μπερδέψει αρκετά ε? :P

Δημοσ.

Καλά το λές με πολυμορφισμό αν πρόκειται να έχεις πολλά τέρατα αλλά τα

else if ( direction == 2 ) και else if ( direction == 3 ) branches είναι αχρείαστα χάνει ο παίκτης το ρυθμό αν γίνονται συχνά interrupts για μηνύματα, θυμίσου είναι παιχνίδι όχι εφαρμογή, θα χάνει το ενδιαφέρον του. Μετά θα πρέπει να βάλεις και γραφικά να μην είναι μόνο text, καλή η προσπάθεια.

Δημοσ.

Δεν ξέρω και πολύ C++, οπότε σκέφτομαι με βάση τη Java, αλλά εναλλακτικά η μέθοδος executeTurn που σκέφτεσαι θα μπορούσε να είναι της κλάσης Dungeοn και να υλοποιείται σύμφωνα με το if (x instanceof Monster) - αν αυτό υποστηρίζεται στη C++. Δεν είναι και τόσο OOD αλλά ίσως να σε βολεύει.

 

Δεν βλέπω τον λόγο βέβαια που χρειάζεσαι το Dungeon να είναι μέλος του Player. Αφού υποτίθεται είναι η "Main Class" σου.

Δημοσ.

Για να μην μπλεξεις τα μπουτια σου. Διαβασε Eυκλειδεια γεωμετρια, κυριως διανυσματα (ή vectors στα εγκλεζικα). Επειτα φτιαξε μια κλαση με ονομα Rasterizer (για να την φτιαξεις αυτη, θα ψαξεις στο google για rasterisation) και τα υπολοιπα ειναι σχετικα ευκολα

Δημοσ. (επεξεργασμένο)

Ο OOP σχεδιασμός είναι δύσκολο πράγμα να γίνει σωστά, και προφανώς δεν υπάρχει "ή σωστή λύση" εκτός από πολύ απλοϊκές περιπτώσεις.

 

Ορίστε μερικές σκέψεις για το πώς θα μπορούσες να το πιάσεις:

  • Ο παίκτης (παίκτες) και τα τέρατα θα ανήκουν σε μια class Actor (όποτε το λέω αυτό εννοώ "ή σε κάποια derived class αυτής, αν έχει νόημα" -- π.χ. θα μπορούσε να υπάρχει μια class Monster : public Actor). Η class αυτή θα περιλαμβάνει πληροφορίες για το συγκεκριμένο τέρας/παίκτη, αλλά όχι πολλή "λογική" (βασικά ίσως και καθόλου λογική, όσο λιγότερη τόσο καλύτερα -- τη λογική βολεύει να τη βάλεις αλλού και μάλιστα όχι όλη στο ίδιο μέρος).
  • Θα υπάρχει μια class World (ή πες την Dungeon αν θέλεις) η οποία θα κρατάει την κατάσταση του παιχνιδιού, οπότε θα κρατάει σίγουρα κάτι σαν vector<Actor>. Βέβαια για κάθε Actor χρειάζεται να ξέρουμε και τη θέση του, οπότε αυτομάτως πάμε σε vector<pair<Actor, Position>>. Και αφού ο κόσμος είναι 2D, προς το παρόν το position μπορεί να είναι ένα pair<int, int> με τις συντεταγμένες.
  • Το σημαντικό είναι πως κάνοντας τα πράγματα έτσι, αν αργότερα αποφασίσεις να αλλάξεις κάτι (π.χ. multilevel dungeon -- τώρα πια δυο συντεταγμένες δε θα φτάνουν) τότε μπορείς να κάνεις την αντίστοιχη αλλαγή στο Position και στον κώδικα που έχει πάρε δώσε μ' αυτή την class (more on that later) και να είσαι κύριος. Αν όμως έχεις μπλεγμένο παντού μέσα στον κώδικά σου ένα access σε 2d πίνακα... καλή τύχη να κάνεις αλλαγές.
  • Ο World πρέπει επίσης να ξέρει και πώς είναι ο "χάρτης", επομένως class Map. Ας πούμε απλοϊκά ότι o Map αναπαριστά ένα παραλληλόγραμο χωρισμένο σε τετράγωνα στα οποία αναφερόμαστε με καρτεσιανές συντεταγμένες. Για τετράγωνο χρειαζόμαστε κατ' ελάχιστον να ξέρουμε "τι είναι εκεί", οπότε ας πούμε enum Terrain { Void, Wall, OpenCorridor, ... }. Εσωτερικά ο Map θα έχει κάτι σαν array<array<Terrain>>, αλλά φυσικά αυτό θα είναι private. Σαν public θα έχει κάτι του στυλ getTerrainAt(Position).
  • Προφανώς με το σύστημα αυτό ήδη αρχίζουμε να φανταζόμαστε πως η Map είναι abstract class και απο κει και πέρα φτιάχνουμε ένα SingleLevelMap για ξεκίνημα, και αργότερα είμαστε έτοιμοι για MultiLevelMap το οποίο στην ουσία θα κρατάει μέσα του πολλά SingleLevelMap που ενώνονται. Βέβαια ο "εξωτερικός κόσμος" δε θα ξέρει τίποτα για όλα αυτά, απλά θα παίζει πάνω στο public interface του Map.
  • Αν θέλεις να έχεις και άλλα "μόνιμα features" πάνω στο χάρτη (π.χ. πόρτες) θα πρέπει να σκεφτείς ξεχωριστά έναν καλό τρόπο να αναπαρασταθούν, έχοντας υπόψη ότι ο Map θα πρέπει να ξέρει μόνο την ύπαρξη μιας αφηρημένης έννοιας "πόρτα" σε κάποιο σημείο. Το γεγονός ότι τώρα η πόρτα είναι ανοιχτή, ή κλειστή, ή έσπασε η κλειδαριά, επειδή είναι κάτι δυναμικό δε θα αποθηκεύεται βέβαια στο Map αλλά κάπου στο World.

Μέχρι εδώ ο World ξέρει το χάρτη και ξέρει τι actors βρίσκονται και που. Μπορείς να φανταστείς ότι κάπου θα χρειάζεται κάτι σαν αυτό:

 

for each Actor
    decide next action

for each action
    calculate the result

// To apply είναι διαφορετικό loop από το calculate για να μπορούμε
// να εφαρμόζουμε τις αλλαγές σα να έγιναν όλα τα actions "ταυτόχρονα" 
for each result
    apply changes to World, Actors, etc

 

Ας πάρουμε πρώτα το "decide next action for each actor". Mπορούμε εδώ να φανταστούμε ότι κάθε Actor θα έχει μια method decideNextAction(const World& w) όπου με δεδομένη την κατάσταση του game world αποφασίζεται το τι θα κάνει ο Actor στο επόμενο turn. Καλό είναι αυτό να γίνει δίνοντας στην class Actor ένα private shared_ptr<BehaviorAlgorithm> όπου η πραγματική δουλειά θα γίνεται μέσα στο αντικείμενο της νέας class BehaviorAlgorithm. Αυτό θα σου επιτρέψει να κάνεις ωραία πράγματα όπως να έχεις διαφορετικά behavior για διαφορετικά τέρατα, να αλλάζεις το behavior ενός τέρατος επι τόπου με ευκολία (π.χ. αν έχει λίγο health από AggressiveBehavior το κάνεις FleeingBehavior κλπ κλπ). Επίσης θα σου επιτρέψει να κρύψεις όλη την ασχήμια του να παίρνεις είσοδο από τον παίκτη μέσα στην class HumanPlayerBehavior. Εκεί, και μόνο εκεί, θα μπλέκεσαι με πληκτρολόγια κλπ.

 

 

 

Αν σκεφτείς τις συνέπειες των παραπάνω, καταλαβαίνεις πως με ένα τέτοιο στήσιμο θα μπορείς απλά να βάλεις non-interactive behavior σε όλους τους actors και να αφήσεις το παιχνίδι "να παίξει μόνο του" έτσι απλά. Αν δεν είναι προφανές γιατί αυτό είναι πολύ επιθυμητό (και άρα το design με το οποίο εύκολα το πετυχαίνεις είναι καλό design), one word: testability.

 

 

 

Επομένως το πρώτο loop γίνεται κάπως σαν:

for (auto actor : this->_actors) {
    auto Action = actor->decideNextAction(*this);
}

Στη συνέχεια θα χρειαστείς και κάτι για να εκτελεί τα actions. Αλλά εδώ ας κάνουμε μια παύση. Με τα "this" παραπάνω αφήνω να εννοηθεί ότι ο κώδικας αυτός (για το παίξιμο του κάθε γύρου) βρίσκεται μέσα στη World. Γιατί; Τι δουλειά έχει ο κώδικας με τα διαδικαστικά μέσα στην class που αποτυπώνει την κατάσταση του παιχνιδιού; Καμία! Οπότε ας φτιάξουμε μια άλλη class Game η οποία θα έχει ένα World και ας βάλουμε τον κώδικα αυτό μέσα στην Game::PlayNextTurn().

 

Για να κάνουμε και το "play" βέβαια πρέπει να υλοποιήσουμε και το επόμενο loop όπου θα γίνεται η ουσιαστική δουλειά (π.χ. ο τάδε actor έκανε move εκεί, ο άλλος έκανε attack, o παράλλος ήπιε potion, κλπ). Πώς θα ξέρουμε ποιά πρέπει να είναι τα αποτελέσματα αυτών των πράξεων? Θέλουμε ακόμα μια class RuleSet. Χρησιμοποιώντας διαφορετικά instances της class αυτής θα μπορείς πολύ εύκολα να αλλάξεις τους κανόνες που διέπουν το παιχνίδι. Το RuleSet λοιπόν θα παίρνει μια λίστα από Action (εδώ είναι που φτιάχνεις ολόκληρη ιεραρχία, abstract class Action, MoveAction : public Action, AttackAction : public Action, κλπ) και μετά θα κάνει visit τις actions αυτές με όποια σειρά θέλει παράγοντας κάποια αποτελέσματα. Αφού παραχθούν όλα τα αποτελέσματα στη συνέχεια θα γίνεται και η εφαρμογή τους (π.χ. ο τάδε Αctor έφαγε ζημιά, το τάδε τετράγωνο του Map άλλαξε κατάσταση γιατί το ανατινάξαμε, κλπ).

 

 

 

Crash course στο Visitor pattern που λέω παραπάνω:

 

Κανονικά σε μια ιεραρχία classes μπορείς εύκολα να προσθέσεις "ουσιαστικά" (classes) αλλά είναι δύσκολο να προσθέσεις "ρήματα" (methods) επειδή θα πρέπει να είσαι σίγουρος ότι μετά την προσθήκη μιας method στην base class, όλες οι derived classes έχουν το σωστό implementation -- κάτι που μπορεί να μην είναι απλό λόγω μεγάλων ιεραρχιών, επειδή κάθε class μπορεί να καλεί τη base implementation ή μπορεί και όχι, και γενικά επειδή αποφάσεις που παίρνεις στο implementation της method σε μία class επηρρεάζουν και όλες τις "απο κάτω".

 

Με το visitor pattern ουσιαστικά αλλάζουμε αυτή την κατάσταση και τα φέρνουμε έτσι που να μπορούμε πολύ εύκολα να προσθέσουμε "ρήματα" (δημιουργείς ένα νέο visitor που αντιστοιχεί στο ρήμα, βάζεις μια Visit(Class c) για κάθε διαφορετικό ουσιαστικό πάνω στο οποίο εφαρμόζεται αυτό το ρήμα, και γράφεις κάθε υλοποίηση ανεξάρτητα από όλες τις υπόλοιπες. Βέβαια τώρα η προσθήκη "ουσιαστικών" είναι τρελλό μανίκι, μιας και θα πρέπει να πας σε όλους τους visitor που έχεις ήδη γράψει, να προσθέσεις μια καινούρια method, κλπ κλπ.

 

Στη συγκεκριμένη περίπτωση τα "ουσιαστικά" είναι τα είδη των κινήσεων (move, attack, use item, cast spell) τα οποία γενικά δεν πρόκειται να αλλάξουν στο μέλλον και τα "ρήματα" είναι το πώς θα μεταφράζονται αυτά τα ουσιαστικά σε αποτελέσματα (π.χ. λυπάμαι πολύ αλλά κάθε Τρίτη ο θεός της μαγείας πάει για μπύρες κι έτσι όλα τα spells κάνουν fizzle), δηλαδή είναι ένας τομέας όπου σίγουρα στο μέλλον θα κάνεις πολλές προσθήκες και αλλαγές. Άρα, visitor.

 

 

 

Θα μπορούσα να συνεχίσω αλλά για να μη χαθεί τελείως η μπάλα σταματάω εδώ. Σαν κατακλείδα θα πω ότι είναι φανερό πως ενώ ξέρεις τους βασικούς μηχανισμούς του OO, δε γνωρίζεις ούτε τους προχωρημένους μηχανισμούς ούτε και το πώς με βάση τους μηχανισμούς σχεδιάζουμε ένα περίπλοκο κατασκεύασμα χωρίς να βγει έκτρωμα. Ξεκίνα από λευκό χαρτί και προσπάθησε να εφαρμόσεις τα παραπάνω βήμα-βήμα. Αφού το καταφέρεις αυτό, βάλε τον καινούριο κώδικα δίπλα δίπλα μ' αυτό που έχεις τώρα, θα έχεις one of those moments "αν είναι ποτέ δυνατόν αυτά τα ξερατά να μου φαινόταν OK".

Επεξ/σία από defacer
  • Like 8
Δημοσ.

Πραγματικά πολύ ενδιαφέρον τον ποστ σου defacer. Ωστόσο όσο το διάβαζα κατάλαβα ότι έχω πολλααα να μάθω.. Αρχικά λοιπόν λέω να κάτσω να ασχοληθώ λίγο με το vector < pair να μάθω πως να το χειρίζομαι καθώς δεν είχα ιδέα...
Θα ακολουθήσω τα βήματα που με παρότρυνες σιγά σιγά...
Η αλήθεια είναι ότι απο εκεί που άρχισες να μου λες για την κλάση BehaviorAlgorithm και τον shared_ptr( αλήθεια γιατί shared? τι εννοείς με αυτό?  ) και μετά χάθηκα λίγο( εώς πολύ στο visitor pattern :P )...

Θα ακολουθήσω όμως τη συμβουλή σου και θα τα πάρω ένα-ένα, βήμα-βήμα.. Αργά και σταθερά... Επειδή σίγουρα θα χρειαστώ και άλλη βοήθεια θα ξανα ποστάρω σε αυτό το thread.

Σε ευχαριστώ πολύ για το χρόνο σου!

Δημοσ.

Λογικό είναι, τα είπα όλα χωρίς πολλές ανάσες στο ενδιάμεσο.

 

Design Patterns

 

Το βασικότερο όλων είναι πως αν βλέπεις σοβαρά το θέμα του προγραμματισμού πρέπει να μάθεις από design patterns. Πρόσφατα διάβασα μια όμορφη εξήγηση του τι είναι design pattern σε ένα blog post του Eric Lippert (πρώην ομάδα C# στη Microsoft, το blog του το συνιστώ και με τα τρία χέρια). Ορίστε λοιπόν πώς τις περιγράφει:

 

 

 

Object-oriented programmers like talking about "design patterns", so let's use that as our framing device.

The idea of "design patterns" comes from real-world-let's-build-a-building architecture. You see the same patterns come up over and over again in architecture in the several millennia that humans have been building stuff: walls, doors, windows, ceilings, columns, vestibutes, courtyards, drawbridges, moats, closets, kitchens and so on. These things all have relationships to each other; a courtyard with no doors and windows is not a very useful courtyard. And these patterns and their relationships are pretty stable over time and space; the Pentagon and a 1900-year old Roman-style villa clearly have many patterns in common despite the vast gulf of time and space between their constructions.

So too with software, we see the same recognizable patterns come up over and over again: functions, variables, types, and so on. Those patterns are so ingrained into our languages, so a part of the air we breathe that we seldom even notice that they are design patterns, but they are. And therefore when we discuss design patterns it is almost always in the context of a pattern that is not a part of the language, but rather must be explicitly implemented by the developer.

 

 

 

Ακριβώς όπως και τα παράθυρα που λέει ο Lippert, αν έχεις διαβάσει αρκετό κώδικα στη ζωή σου έχεις δεί σίγουρα design patterns σε δράση (όπως έχεις δει και παράθυρα). Αν θέλεις όμως να χτίσεις σπίτια, πρέπει να ξέρεις περισσότερα από το να αναγνωρίζεις ένα παράθυρο όταν το δεις.

 

Για να μάθεις από design patterns το σύστημα είναι το ίδιο όπως και με οτιδήποτε άλλο στον προγραμματισμό:

  1. Διαβάζεις (για να ξέρεις για τι πράγμα μιλάμε)
  2. Γράφεις προγραμματάκια για ζέσταμα όπου τις χρησιμοποιείς όπως όπως (για να ξέρεις με τι μοιάζει σε κώδικα)
  3. Διαβάζεις σοβαρό κώδικα τρίτων (για να δεις πώς χρησιμοποιούνται και στην πράξη)

Για διάβασμα θα βρεις και online πηγές, αλλά η βίβλος είναι το λεγόμενο Gang of Four book (αναφέρεται και ως GoF) το οποίο συστήνω ανεπιφύλακτα να το αγοράσεις -- στην αρχή θα πέσει βαρύ και δεν είναι από αυτά που τα πιάνεις από την αρχή και τα τελειώνεις, αλλά από την άλλη είναι ένα βιβλίο που θα σου μαθαίνει πράγματα ακόμα και μετά από χρόνια.

 

Αυτό που έλεγα παραπάνω για BehaviorAlgorithm είναι στην ουσία το Strategy pattern. To Visitor το είδαμε λίγο εκτενέστερα ήδη. Οι διάφοροι Actors θα μπορούσαν (αν και δε χρειάζεται όταν δουλεύουμε σε μικρή κλίμακα) να υλοποιούνται με τη χρήση Flyweight, το οποίο με τη σειρά του χρειάζεται Factory. You get the picture.

 

C++

 

H C++ είναι δύσκολη γλώσσα, δε θα την πρότεινα για να μάθει κανείς. Και είναι δύσκολη γιατί έχει μέσα την Άρτα και τα Γιάννενα, π.χ.:

  • low-level programming model, "close to the metal" που λένε όπως η C
  • OO features (προφανώς) και μάλιστα ένα ιδιαίτερο κοκτέιλ αυτών (δε σου κάνω τη χάρη να έχω την έννοια του interface, τα απαιτούμενα συστατικά τα έχεις, φτιάξτο μόνος σου -- από την άλλη έχω multiple inheritance και καλά ξεμπερδέματα)
  • generic programming (μέσω templates, π.χ. std::vector<X> όπου Χ "ο,τι θές εσύ")
  • template metaprogramming (προσοχή, μόνο για έμπειρα αγόρια -- δεν υπερβάλλω)

Βάλε σ' αυτά και το ότι είναι πολύ εύκολο να κάνεις "λάθος πράγματα" προκαλώντας UB και το συμπέρασμα δε βγαίνει δύσκολα.

 

Αν θέλεις να μάθεις C++ λοιπόν πρέπει επίσης να διαβάσεις. Σε πρώτη φάση αυτό σημαίνει ότι πρέπει να μάθεις σταδιακά τα διάφορα χρήσιμα πράγματα που περιλαμβάνει η standard library, τα οποία περιλαμβάνουν όλα αυτά που ανέφερα παραπάνω: vector, array, shared_ptr, pair (και τη γενίκευσή του, tuple) και πάρα πολλά ακόμα (π.χ. θα μπορούσα να πω ότι αν δεν έχεις χρησιμοποιήσει τουλάχιστον τα μισά από αυτά που περιέχει ο header <algorithm>, δεν "ξέρεις C++" ούτε γι' αστείο).

 

Καλό θα ήταν να πάρεις κάποιο βιβλίο ή να βρεις κάποια σειρά άρθρων (δυστυχώς δεν έχω κάτι υπόψη) που να σε κάνει ένα tour τόσο στη standard library όσο και στις νέες δυνατότητες της C++11 τις οποίες πιθανότατα δεν γνωρίζεις. Αλλά πάνω απ' όλα πρέπει να γράφεις κώδικα και επίσης να διαβάζεις κώδικα (άλλων). Προφανές νομίζω, δε μπορεί να γίνει κανείς ούτε αρχιτέκτονας χωρίς να μελετήσει τα σχέδια άλλων ούτε να σχεδιάζει μηχανές αυτοκινήτων χωρίς να έχει μελετήσει πρώτα αυτές που σχεδίασαν άλλοι στο παρελθόν.

 

Κάποια πράγματα που θα δεις δε θα καταλάβεις γιατί γίνονται έτσι επειδή δε θα μπορείς να φανταστείς λόγω περιορισμένης εμπειρίας την κατάσταση στην οποία μας είναι πολύ χρήσιμο το να έχουν γίνει έτσι -- είναι απόλυτα φυσιολογικό, αλλά όταν γίνει ρώτα για να μάθεις.

 

Και μιας και ήδη ρώτησες, ο shared_ptr είναι ένα είδος smart pointer, μπορείς να δεις παραδείγματα για τη χρήση του εδώ. Αν αρχίσεις να τον χρησιμοποιείς, κάνε πρώτα μια παύση για να μάθεις για τα αδερφάκια του unique_ptr και weak_ptr γιατί για κάθε δουλειά το κατάλληλο εργαλείο είναι διαφορετικό. Συγκεκριμένα, το shared_ptr είναι το πιο "εύκολο" αλλά το unique_ptr είναι στις περισσότερες περιπτώσεις υπεραρκετό.

 

Source Control

 

Δε νοείται να γράφεις προγράμματα και να μη χρησιμοποιείς κάποιο εργαλείο source control. Χρησιμοποιείς;

 

Αν όχι, σταμάτα ο,τι κάνεις επιτόπου και κατέβασε το Mercurial. Στη συνέχεια πήγαινε και διάβασε (κάνε) αυτό το tutorial και την επόμενη εβδομάδα παίξε με το καινούριο παιχνίδι για να αποκτήσεις μια σχετική άνεση. Όταν ξεπεράσεις το στάδιο "πώς ήταν δυνατόν να δούλευα πριν χωρίς αυτό το πράγμα" μπορείς να περάσεις στο επόμενο βήμα: κάνε ένα λογαριασμό στο BitBucket. Είναι δωρεάν και σου επιτρέπει να έχεις αποθηκευμένα in the cloud τα προγράμματά σου (σαν extra copy, γιατί βέβαια και στο δικό σου υπολογιστή θα έχεις ένα τελείως ισοδύναμο αντίγραφό τους οπότε δεν υπάρχει τίποτα να σε ανησυχεί) και αν το επιλέξεις να δώσεις σε τρίτους τη δυνατότητα να τα δουλέψουν ή να τα βλέπουν χρησιμοποιώντας όλα τα εργαλεία που δίνει το mercurial. Επομένως το "ο κώδικάς μου είναι αυτός" θα μπορείς πλέον να το κάνεις κάπως έτσι (τυχαίο παράδειγμα).

 

Σου πρότεινα το mercurial γιατί είναι κάπως ευκολότερο να το πιάσεις. Η αλήθεια όμως είναι ότι στην πράξη πολύς περισσότερος κόσμος χρησιμοποιεί git και ο μισός πλανήτης το αντίστοιχο με το BitBucket site, το GitHub. To mercurial με το git έχουν διαφορές, αλλά η γενική φιλοσοφία τους είναι πολύ πολύ παρόμοια. Το 80-90% από αυτά που θα μάθεις με το hg είναι γνώση που μεταφέρεται αυτόυσια και στο git.

 

Happy coding!

  • Like 6

Δημιουργήστε ένα λογαριασμό ή συνδεθείτε για να σχολιάσετε

Πρέπει να είστε μέλος για να αφήσετε σχόλιο

Δημιουργία λογαριασμού

Εγγραφείτε με νέο λογαριασμό στην κοινότητα μας. Είναι πανεύκολο!

Δημιουργία νέου λογαριασμού

Σύνδεση

Έχετε ήδη λογαριασμό; Συνδεθείτε εδώ.

Συνδεθείτε τώρα
  • Δημιουργία νέου...