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

[C] - Minesweeper clone


bnvdarklord

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

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

Download: minesweeper_mingw32_03.7z (57.5Kb)

 

grW8wRh.png?1

Πρώτα τα εύκολα, προσέθεσα στο win32 gui τρεις επιπλέον λειτουργίες: F1 (σύντομες οδηγίες), F2 (cycling μεταξύ αγγλικών κι ελληνικών) και Esc (τερματισμός). Η τρέχουσα γλώσσα καθώς και το τρέχον μέγεθος του ταμπλό εμφανίζονται στον τίτλο του παραθύρου του παιχνιδιού.

 

Τώρα στα πιο τεχνικά για το win32 gui.

 

Ο κώδικας μετατράπηκε πλήρως σε καθαρή C (βασικά ελάχιστα ήταν C++ από την αρχή) οπότε έφτιαξα και 2ο makefile (mingw32_c.mak) με το οποίο γίνονται compile ως καθαρή C τα .cpp αρχεία.

 

Αν είστε σε Linux, μπορεί να χρειαστεί να απενεργοποιήσετε το gcc/g++ flag -finput-charset=cp1253 διότι λογικά σε εσάς τα πηγαία αρχεία θα ανοίγουν ως UTF8 (που είναι και το default encoding που θεωρεί ο mingw32 gcc/g++ όταν δεν υπάρχει το παραπάνω flag.

 

Το παιχνίδι ξεκινάει στα αγγλικά (και γίνεται toggle με ελληνικά με το F2). Αν θέλετε να ξεκινάει με ελληνικά, ορίστε το preprocessor directive LANG σε LANG_EL κατά το compilation.

 

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

/* για C++ */
make LANG=-DLANG_EL -f mingw32.mak

/* για C */
make LANG=-DLANG_EL -f mingw32_c.mak
Βασικά οτιδήποτε άλλο πλην LANG_EL κάνει fallback σε αγγλικά. Η ύπαρξη 2 γλωσσών στο παιχνίδι είναι τελείως αυτόνομη, καμία σχέση δηλαδή με την internationalization διαδικασία των win32 εφαρμογών. Έβαλα 2 γλώσσες επειδή ήταν πανεύκολο να τις "χώσω" μέσα στον κώδικα. Άλλωστε τα strings είναι ελάχιστα (μονάχα ο τίτλος του παραθύρου και το κείμενο των σύντομων οδηγιών).

 

Σχετικά με τις καθολικές μεταβλητές, όχι μόνο άφησα όσες είχε ήδη ο bnvdarklord, αλλά προσέθεσα κι άλλες (βασικά ότι έχει σχέση με τις γλώσσες). Από ότι θυμάμαι, το win32 api έχει έναν περίεργο μηχανισμό να περνάς user-data στην CreateWindowEx (κάτι με void pointer ως τελευταίο όρισμα, αλλά που πρέπει να έχει μια συγκεκριμένη μορφή, συν να περάσεις κι ένα ειδικό flag για να καταλάβει ότι πρόκειται για user-data, και μετά να πας να τα σώσεις αλλού με άλλο function, και κάτι τέτοια). Βασικά δεν ασχολήθηκα καθόλου, τα έβαλα global και πάπαλα. Αλλά ως global τα χρησιμοποιώ μονάχα στην WinMain(), σε όλες τις υπόλοιπες συναρτήσεις περνιούνται ως ορίσματα, μόνο αν χρειάζονται.

 

Σχετικά με τα 3 νέα keys (F1, F2, Esc), φίλε bnvdarklord τα έχω μαζέψει όλα σε μια συνάρτηση (για να είναι πιο διακριτά από τα υπάρχοντα δικά σου) κι επίσης τα διαχειρίζομαι με το WM_KEYDOWN message, αντί για το WM_KEYUP με το οποίο διαχειρίζεσαι τα δικά σου.

 

Τέλος, η _Win32RefreshStrings() που φτιάχνει και εμφανίζει τον τίτλο του παραθύρου, ανάλογα την ενεργή γλώσσα, τρέχει σε κάθε iteration του main-loop. Βαρέθηκα να το κάνω επιλεκτικό, γιατί θα έπρεπε να αλλάξω αρκετό ακόμα από τον υπάρχοντα κώδικα.

 

Τέλος, ότι προσθήκες έχω κάνει στο win32_minesweeper.cpp τις έχω "μαρκάρει" με /* migf1: */ comments.

 

Πάμε τώρα στα υπόλοιπα αρχεία, τα του κυρίως παιχνιδιού.

 

Εκεί κατάργησα όλες τις καθολικές μεταβλητές. Βασικά ήταν όλες στο game.cpp με internal linkage (πρώην mine_game.cpp). Τις αντικατέστησα με static μεταβλητές μέσα στην game_update_and_render() και από εκεί δρομολογούνται στις υπόλοιπες συναρτήσεις ως ορίσματα, άλλού by-value κι αλλού by-reference (ως δείκτες εννοώ). Σαφώς και υπάρχει μεγαλύτερο overhead έτσι (καθώς και περισσότερος κώδικας), αλλά είναι πια όλες περιορισμένες στο scope της game_update_and_render() και άρα πιο εύκολα "παρακολουθίσιμες". Ζυγίζει κανείς τα υπέρ και τα κατά κι επιλέγει. Προσωπικά προτιμώ να τις αποφεύγω τις καθολικές όσο (και όταν) μπορώ, για αυτό και το έκανα έτσι.

Προφανώς δεν πρόκειται περί άσπρου-μαύρου. Μπορεί κάποιες να μείνουν global κι άλλες static.

 

Η μόνη καθολική που δεν μετέτρεψα σε static μέσα στην game_update_and_render() ήταν η _g_gameoptions. Αυτήν την κατάργησα τελείως από τα αρχεία του παιχνιδιού, και στη θέση της έφτιαξα ένα opaque type GameOptions μαζί με constructor, destructor και getters, ώστε να μπορεί το gui αρχείο να ορίζει και να διαχειρίζεται GameOptions μεταβλητές (βασικά δείκτες) χωρίς να χρειάζεται να γνωρίζει τα implementation details του datatype.

 

Το έκανα έτσι ως παράδειγμα ενός άλλου τρόπου "συνεννόησης" του παιχνιδιού με το gui, συγκριτικά με αυτόν του αρχικού κώδικα.

 

Να το εξηγήσω λίγο καλύτερα. Ο υπάρχον τρόπος έκανε expose στο gui τα implementation details του κυρίως παιχνιδιού, τα οποία έπρεπε να τα γνωρίζει το gui προκειμένου να τα χρησιμοποιήσει.

 

Ας πάρουμε για παράδειγμα το struct GameMemory. Ο ορισμός του γίνεται μέσα στο game.h και είναι public διαθέσιμος σε οποιοδήποτε άλλο αρχείο κάνει #include "game.h".

 

 

#ifndef GAME_H
#define GAME_H
...
typedef struct GameMemory   GameMemory;
struct GameMemory
{
	bool isInited;

	/* [NOTE]
	 * This stores the whole spritesheet at first
	 * and then the game board. Any leftover memory
	 * is used as a stack for the flood fill.
	 */
	void *trans;        // transient storage
	int  transsize;

	/* [NOTE]
	 * This stores the sprites.
	 */
	void *perm;         // permanent storage
	int  permsize;
};
#endif

 

 

Στη δική μας περίπτωση, το include αυτό γίνεται στο win32_minsweeper.cpp το οποίο κατόπιν ορίζει (στην stack) μια μεταβλητή τύπου GameMemory:

			GameMemory gamememory;
			ZeroMemory( &gamememory, sizeof(gamememory) );
Κατόπιν την διαχειρίζεται με ακριβή γνώση της υλοποίησης του struct GameMemory. Κάνει δηλαδή απευθείας access τα fields του struct GameMemory, π.χ.:

			gamememory.permsize  = KILOBYTES( 64 );
			gamememory.transsize = KILOBYTES( 512 );
Αν αλλάξει το internal implementation του struct GameMemory (να σου πω το πιο απλό, να αλλάξει το όνομα του field ή να αλλάξει ο τύπος του), τότε θα πρέπει υποχρεωτικά να αλλάξει και ο κώδικας του gui.

 

Αν το gui έκανε access τις πληροφορίες που ήθελε μέσω ενός getter χωρίς να γνωρίζει το internal implementation, τότε ο κώδικας του gui δεν θα χρειαζόταν να αλλάξει αν αλλάξει το internal implementation του .perm και .permsize, και γενικότερα του struct GameMemory. Θα αρκούσε όταν κάναμε τις αλλαγές στο internal implementation να σιγουρευόμασταν πως οι πληροφορίες που επιστρέφει ο getter στο gui παραμένουν ίδιες.

 

Aς πούμε ότι το κυρίως παιχνίδι παρέχει έναν GameMemory getter:

void *game_memory_get_permanent( GameMemory *memory );
τον οποίον τον χρησιμοποιεί ήδη το gui (ή οποιοσδήποτε άλλος). Αν πάμε και αλλάξουμε στο struct GameMemory το field perm σε permanent, και προσαρμόσουμε αντίστοιχα των εσωτερικό κώδικα του getter, τότε ο υπάρχων κώδικας του gui παραμένει ανέπαφος. Χαζό παράδειγμα, αλλά νομίζω δίνει τη γενική ιδέα.

 

Αυτό το information-hiding, στη C γίνεται με τα λεγόμενα opaque data types. Δηλαδή, αντί να κάνουμε expose στο public header file την υλοποίηση του struct GameMemory, κάνουμε expose μονάχα το όνομά του, με forward declaration. Επίσης, (ιδανικά) στο ίδιο header file κάνουμε publicly expose και τα function prototypes του constructor, του destructor, των getters, κλπ, κλπ, που αφορούν το συγκεκριμένο opaque data type.

 

Στον κώδικα που δίνω σε .7z στην κορυφή του πόστ, έχω κάνει αυτό το πράγμα με το struct GameOptions.

 

Δηλαδή το internal-implementation τόσο του struct όσο και των manager/accessors/mutators του γίνονται privately στο game.cpp:

 

 

/* game.cpp */
...
struct _GameOptions
{
	int xboard;   // board-width  in tiles
	int yboard;   // board-height in tiles
	int nmines;   // total count of mines in the board
};
...
/***********************************************//**
 * Constructor, destructor & getters implementation
 * for the opaque datatype: GameOptions (declared in
 * game.h).
 ***************************************************
 */
struct _GameOptions *game_options_new( void )
{
	struct _GameOptions *ret
	= (struct _GameOptions *) calloc( 1, sizeof(*ret) );

	return ret;
}

void game_options_free( struct _GameOptions **options )
{
	if ( NULL != options ) {
		free( *options );
		*options = NULL;
	}
}

int game_options_get_xboard( const struct _GameOptions *options )
{
	if ( NULL == options ) {
		return -1;
	}

	return options->xboard;
}

int game_options_get_yboard( const struct _GameOptions *options )
{
	if ( NULL == options ) {
		return -1;
	}

	return options->yboard;
}
...

 

Δημόσια exposed στο game.h γίνονται μονάχα τα declarations τους:

 

 

#ifndef GAME_H
#define GAME_H
...
/*
 * Forward declarations of Opaque custom types (defined in game.cpp)
 */
typedef struct _GameOptions GameOptions;
...
/* constructor, destructor & getters for the opaque type: GameOptions */
extern GameOptions *game_options_new( void );
extern void        game_options_free( GameOptions **options );
extern int         game_options_get_xboard( const GameOptions *options );
extern int         game_options_get_yboard( const GameOptions *options );
...

 

Όταν έρθει τώρα το gui (ή όποιος άλλος) και κάνει #include "game.h", δεν αποκτά την παραμικρή ιδέα για το internal implementation. Ξέρει μονάχα πως μπορεί να ορίσει μεταβλητές τύπου GameOption και ότι για να τις διαχειριστεί πρέπει να χρησιμοποιήσει τις παρεχόμενες συναρτήσεις.

 

Στη C, τα opaque data types δεν μπορούν να γίνουν instantiate στην stack, παρά μόνο ως δείκτες στη heap, γιατί επί της ουσίας πρόκειται για incomplete types. Επίσης, οι δείκτες αυτοί δεν μπορούν να γίνουν dereference (για αυτό χρειάζονται getters, setters, constructor, destructor, κλπ συναρτήσεις... εσωτερικά σε αυτές τις συναρτήσεις είναι complete το type, μιας και ο ορισμός τους βρίσκεται στο ίδιο source-module).

 

Ήθελα λοιπόν στο gui (win32_minesweeper.cpp) να τραβήξω από το κυρίως παιχνίδι το πλάτος και το μήκος του ταμπλό, προκειμένου να τα εμφανίσω στην γραμμή τίτλου. Αντί να κάνω δημόσια expose ολόκληρο το GameOptions στο "game.h", το έκανα ως opaque datatype, μαζί με constructor, destructor και 2 getters (έναν για το πλάτος κι έναν για το ύψος του ταμπλό).

 

Οπότε στο gui (στην WinMain())το έκανα "ας πούμε instantiate" στην heap σε δείκτη, μέσω του constructor του:

...
				/* [TODO] migf1:
				 * The following should be checked for failure.
				 */
				GameOptions *gameoptions = game_options_new();

				while ( _g_Running )
				{
...
Κατόπιν στην Win32RefreshStrings() που δημιουργώ τον τίτλο του παραθύρου, "τραβάω" το πλάτος και το ύψος του ταμπλό μέσω των παρεχόμενων getters (τα 2 τελευταία ορίσματα της _sntprintf()):

 

 

 

/***********************************************//**
 * migf1:
 * Refresh all the gui strings, according to the langId parameter.
 * The strings[] array holds the strings of all supported languages,
 * while gameoptions is used for displaying the size of the board.
 *
 * Currently only the window's title gets updated here. That's because the
 * only other strings are the ones displayed in the QuickHelp message-box.
 * Those strings are selected & displayed in the func: _Win32ShowQuickHelp()
 ***************************************************
 */
static bool _Win32RefreshStrings(
	HWND  Window,
	int   langId,
	const struct _win32_strings strings[],
	const GameOptions *gameoptions
	)
{
	TCHAR szBuffer[BUFSIZ] = { _T('\0') };

	if ( NULL == strings || NULL == gameoptions ) {
		return false;
	}

	/* construct and set the window's title */
	_sntprintf(      // this function is provided by the win32 api
		szBuffer,
		sizeof(szBuffer) / sizeof(TCHAR),
		_T( "%s (%s) - %s: %dx%d" ),
		strings[langId].appname,
		strings[langId].langname,
		strings[langId].boarddims,
		game_options_get_xboard( gameoptions ),
		game_options_get_yboard( gameoptions )
		);
	return SetWindowText( Window, szBuffer );
}

 

Τέλος, όταν τελειώνει το main-loop του gui, καλώ τον destructor του:

...
					LastCounter = EndCounter;
				} // while

				game_options_free( &gameoptions );
				...
Ιδανικά, το κυρίως παιχνίδι θα έπρεπε να κρύβει τα internal implementation από το gui, και να του παρέχει μονάχα όσες πληροφορίες χρειάζεται, μέσω ενός ή περισσοτέρων opaque datatypes. Για το συγκεκριμένο παιχνίδι, που είναι πολύ μικρό, θεωρώ πως χρειάζεται μονάχα ένα opaque datatype (π.χ. struct Game ή struct GameUi) οργανωμένο έτσι ώστε να παρέχει μονάχα τις πληροφορίες που χρειάζονται για να φτιαχτεί ένα ή περισσότερα gui's, χωρίς να κάνει expose τα internal implementations του. Γι αυτό με βλέπεις και το τρενάρω να κοιτάξω διεξοδικά τον κώδικα. Επειδή είναι φτιαγμένος με άλλη λογική, οπότε το πάω κι εγώ βήμα-βήμα :)

 

Π.χ. τις πληροφορίες που ήθελα για τον τίτλο του παραθύρου, δλδ το πλατος και το ύψος του ταμπλό, μάλλον τις κάνεις ήδη expose στο gui με κάποιον τρόπο (πιθανότατα με exposed & direct access στο internal implementation κάποιου από τα struct) αλλά προτίμησα να γράψω στα γρήγορα ένα opaque datatype, γιατί μου φαίνεται πως τις έχεις κάπως διάσπαρτες τις πληροφορίες, καθώς επίσης πως τις έχεις και duplicted. Π.χ. αν δεν κάνω λάθος, το πλάτος και το ύψος του ταμπλό υπάρχουν και στο GameOptions, και στο GameBoard (ενδεχομένως κι αλλού). Δεν έχεις και σχόλια στον κώδικα και δεν είναι πολύ εύκολο :P

Επεξ/σία από migf1
  • Like 1
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Καλημέρες!

 

Φίλε bnvdarklord, βλέπω στον κώδικα του win32_minsweeper.cpp κάτι "περίεργα" και θα ήθελα κάποιες διευκρινήσεις (όποτε μπορέσεις).

 

Καταρχήν, είναι συνειδητή η λογική ασυνέπεια μεταξύ ονομασιών & υλοποίησης των perm, permsize, trans και transsize;

 

Από ότι είδα, ως permanent storage (perm) κάνεις allocate (permsize + transsize) bytes, οπότε το permsize φαντάζει πλέον misleading στα μάτια μου.

 

Κατόπιν, το transient storage (trans) δεν είναι ξεχωριστό κομμάτι μνήμης όπως είχα υποθέσει βλέποντας τον ορισμό του στο struct GameMemory. Το είχα υποθέσει γιατί ως διάταξη & ορισμός, τα perm & permsize είναι ολόιδια με τα trans & transsize, και δεν υπάρχει κάποια ένδιεξη ή σχόλιο που να τα καθιστά διαφορετικά . Είδα όμως μέσα στο win32_minesweeper.cpp πως τελικά το trans είναι απλώς ένας pointer, που τον βάζεις να δείχνει στο τέλος του permsize (που στην ουσία δεν είναι permsize :P ).

 

Δηλαδή, συνοψίζοντας, από ότι είδα χτες βράδυ, κάνεις allocate 64 + 512 kbytes μνήμης και τα εκχωρείς στον perm, αλλά ως permsize ορίζεις τα 64 kbytes (και όχι τη σούμα των 576 kbytes που θα περίμενε κανείς ως φυσιολογικό). Κατόπιν απλώς βάζεις τον δείκτη trans να δείχνει στην αρχή του 65ου kbyte και ορίζεις ως transsize τα 512 kbytes (ενώ εγώ περίμενα πως και ο trans θα έδειχνε σε ξεχωριστά allocated μνήμη).

 

Σωστά το έχω καταλάβει μέχρι εδώ;

 

Μια 2η απορία που μου δημιουργήθηκε βλέποντας χτες τον κώδικα του win32_minesweeper.cpp, είναι γιατί κάνεις allocate την μνήμη του perm με την win32 specific VirtualAlloc(); Υπάρχει δηλαδή κάποιος ιδιαίτερος λόγος η μνήμη του κυρίως παιχνιδιού αντί να γίνεται allocated με στανταρ, cross-platform ρουτίνες, να γίνεται allocate με ρουτίνες που είναι specific στο api του gui;

 

Διευκρίνησέ μου όποτε ευκαιρήσεις τα παραπάνω, γιατί σκέφτομαι να κάνω και το GameMemory opaque datatype (να μην ξέρει δλδ το GUI την εσωτερική του υλοποίηση) οπότε θα ήθελα να ξέρω τι είδους mini-api για το GameMemory θα κάνω expose δημόσια στο game.h.

 

Δηλαδή, αν όντως ο trans είναι πάντα εξαρτημένος από τη μνήμη που έχει ήδη γίνει allocated για τον perm, τότε ο destructor του GameMemory δεν πρέπει να κάνει free() τον trans, αλλά μόνο τον perm.

 

Επιπρόσθετα, αν υπάρχει ιδιαίτερος λόγος το allocation μνήμης για τον perm να γίνεται έξω από το cross-platform context του κυρίως παιχνιδιού, τότε ο constructor του GameMemory θα πρέπει να δέχεται ως όρισμα μια callback συνάρτηση που θα είναι ο εξωγενής allocator (αλλιώς το allocation θα γίνεται με στάνταρ calloc μέσα στο context του κυρίως παιχνιδιού).

 

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Σχετικά με την VirtualAlloc την χρησιμοποιώ επειδή είδα να την χρησιμοποιεί και ο τύπος στο handmade hero στο windows layer του. Όταν τον ρώτησε κάποιος απάντησε ότι τελικά οι malloc κτλ σε περιβάλλον windows καταλήγουν να καλούν την VirtualAlloc, και αν δεν κάνω λάθος και θυμάμαι σωστά υπονόησε ότι καλώντας απευθείες την VirtualAlloc όπως θες είναι αποδοτικότερο ή/και έχεις περισσότερο έλεγχο. Τελικά δεν έχει και πολύ σημασία φαντάζομαι.

 

Σχετικά με την μνήμη. Το PermanentStorage και το TransientStorage είναι δίπλα δίπλα στην μνήμη. Με ένα VirtualAlloc δεσμεύω τον χώρο και για τα δύο (η δεσμευση γίνεται μεγέθους TotalMemorySize όπως θα είδες). Μετά προσαρμόζω τον δείκτη του TransientStorage να δείχνει στην αρχή του (μετά τα 64ΚΒ).

 

Θα μου πεις γιατί να μην κάνω 2 VirtualAlloc και κάνω ένα και για τα δύο. Το GameMemory σαν λογική την έμαθα και αυτή από το handmade hero. Η λογική του τύπου είναι ότι δεσμεύεις από την αρχή την μνήμη που θα χρειαστείς(φτιάχνοντας ένα παιχνίδι ξέρεις πόση θα χρειαστείς) ώστε να ελαχιστοποιήσεις τα failure points στο πρόγραμμα σου. Η δεσμευση μνήμης στο runtime είναι ένα πιθανό failure point για αυτό εκείνος την δεσμεύει όλη στην αρχή. Με την λογική αυτή αντί για 2 VirtualAlloc κάνεις 1. Ίσως φαίνεται λεπτομέρεια, αλλά το GameMemory θα μπορούσε να είναι partitioned σε περισσότερα τμήματα, οπότε αντί να υπάρχουν 5-10 allocs υπάρχει ένα για όλα, και απλά φτιάχνεις τους δείκτες μετα.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Σχετικά με την VirtualAlloc την χρησιμοποιώ επειδή είδα να την χρησιμοποιεί και ο τύπος στο handmade hero στο windows layer του. Όταν τον ρώτησε κάποιος απάντησε ότι τελικά οι malloc κτλ σε περιβάλλον windows καταλήγουν να καλούν την VirtualAlloc, και αν δεν κάνω λάθος και θυμάμαι σωστά υπονόησε ότι καλώντας απευθείες την VirtualAlloc όπως θες είναι αποδοτικότερο ή/και έχεις περισσότερο έλεγχο. Τελικά δεν έχει και πολύ σημασία φαντάζομαι.

Το κομμάτι του έχεις περισσότερο έλεγχο είναι λογικό αλλά το αποδοτικότερο είναι σίγουρο ? Αν δεν μιλάμε για κάποιο πολύ μεγάλο μέγεθος που η malloc θα καλέσει HeapAlloc οπότε και VirtualAlloc (σε linux συχνά είναι 2MB το όριο, σε windows δεν γνωρίζω), τότε η malloc δεν θα είναι πιο γρήγορη ? Θα πάρεις ως αποτέλεσμα κάποια περιοχή από την ήδη έτοιμη pool αντί να τρέξουν αργά system calls και να ζητηθεί μνήμη από τον πυρήνα.

 

Ακόμη και μεγάλο ποσό να ζητήσεις οπότε να τρέξει από πίσω η VirtualAlloc, θα είναι τόσο μεγάλο το overhead ? Εκτός αν θέλεις να μεταχειρίζεσαι τη μνήμη σε πολλές dlls ή αν χρειάζεσαι μεγαλύτερη alignment από 64/128bit οπότε η malloc δεν σου κάνει και πάω πάσο.

 

Πολύ σοβαρό το φόρουμ του προγραμματισμού! 50 απαντήσεις μπουρδολογία και μόλις κάποιος βάλει κώδικα τουμπεκί όλοι... Ντα φακ.

Δεν απάντησα νωρίτερα γιατί δεν είχα και πολύ χρόνο. Το μόνο πρόβλημα που είδα είναι το παρακάτω:

 

if((i * 2 + 1) > row || (j * 2 + 1) > col) {
Το i το χρησιμοποιείς για την οριζόντια διάσταση αλλά εδώ ελέγχεις την μεταβλητή row αντί για την col που είναι οι στήλες. Αντέστρεψα τον έλεγχο και παίζει κανονικά. Από εκεί και πέρα δεν έκανα κάτι ιδιαίτερο απλά πρόσθεσα ένα μικρό Makefile, έσπασα τον κώδικα σε διαφορετικά αρχεία σε ενότητες και άλλαξα τις sscanf με μια κλήση της getopt για το parsing των παραμέτρων. Όταν βρω λίγο χρόνο να το πειράξω και άλλο θα γράψω τη συνέχεια εδώ.
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Το κομμάτι του έχεις περισσότερο έλεγχο είναι λογικό αλλά το αποδοτικότερο είναι σίγουρο ?

Δεν το είπα με σιγουριά, βρήκα όμως το video που το εξηγεί όταν τον ρωτήσαν. Είπε ότι απλά η malloc μετά από αχρείαστο κώδικα θα καλέσει την VirtualAlloc. Δεν πολύ ξέρω από αυτά για να είμαι ειλικρινής, αλλά σίγουρα ξέρει γιατι πράγμα μιλάει. Το κομμάτι από το video που το εξηγεί είναι : youtube.com/watch?v=hNKU8Jiza2g&t=1h05m στη 1ώρα και 5 λεπτά.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Δεν το είπα με σιγουριά, βρήκα όμως το video που το εξηγεί όταν τον ρωτήσαν. Είπε ότι απλά η malloc μετά από αχρείαστο κώδικα θα καλέσει την VirtualAlloc. Δεν πολύ ξέρω από αυτά για να είμαι ειλικρινής, αλλά σίγουρα ξέρει γιατι πράγμα μιλάει. Το κομμάτι από το video που το εξηγεί είναι : youtube.com/watch?v=hNKU8Jiza2g&t=1h05m στη 1ώρα και 5 λεπτά.

Ναι αυτό που σου είπα και εγώ για το overhead λέει. Υπάρχουν δεκάδες major υλοποιήσεις για allocator οπότε δεν μπορούμε να πούμε ότι "έτσι δουλεύει το πράγμα" αλλά μια λειτουργία είναι να υπάρχει ένα pool με περιοχές διαφόρων μεγεθών και η malloc να σου δίνει μία περιοχή από αυτές. Αν το μέγεθος που ζητάς είναι μεγαλύτερο τότε πρέπει να ζητήσει μνήμη από το λειτουργικό. Δηλαδή να το πούμε μπακάλικα, αν ζητήσεις 100MB με την malloc, θα τρέξει κάποια if και μετά θα τρέξει την HeapAlloc και κατά συνέπεια την VirtualAlloc. Έτσι το σκεπτικό του είναι ότι τρέχοντας κατευθείαν την VirtualAlloc γλυτώνουμε αυτό το χρόνο το οποίο δεν είναι παράλογο. Η απορία που είχα εγώ είναι αυτοί οι έλεγχοι που θα κάνει η malloc παίρνουν όντως τόση πολύ ώρα ώστε τρέχοντας κατευθείαν την VirtualAlloc να έχουμε τόσο μεγάλο κέρδος που να αξίζει το κόπο ?

 

Όσον αφορά το "σίγουρα ξέρει για τι πράγμα μιλάει", με κίνδυνο να κατηγορηθώ πάλι ότι προσπαθώ να μειώσω τον τύπο του HH (κάτι που ποτέ δεν έκανα), να πω ότι στο video που έδωσες λέει μετά ότι ίσως η HeapAlloc είναι system call οπότε είναι αργή και ότι δεν ξέρει ακριβώς τι γίνεται γιατί δεν χρησιμοποιεί αυτούς τους τρόπους (δηλαδή τα malloc, new, HeapAlloc μια και η ερώτηση ρωτούσε τι διαφορές έχουν). Άρα δηλαδή είναι λιγότερο ότι "σίγουρα το ξέρει και το λέει" και περισσότερο "χρησιμοποιώ από συνήθεια την VirtualAlloc γιατί αυτήν έμαθα και εφόσον κιόλας είναι η πιο γρήγορη δεν βλέπω λόγο να την αλλάξω".

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Ναι κατάλαβα τι λες. Ίσως το κάνει κιολας επειδή ξέρει ότι σίγουρα δεν θα πάρει απο τα pools που λες, καθώς δεν μιλάμε για ναρκαλιευτή που ζητάει λιγα kilobytes. Στο παιχνίδι που φτιάχνει δηλαδή ζητάει μνήμη τάξης GB.

 

 

Όσον αφορά το "σίγουρα ξέρει για τι πράγμα μιλάει"

Ναι, εννούσα περισσότερο στα πράγματα που έχει σταθερή άποψη - πχ γιατί VirtualAlloc και όχι malloc.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Μόνο που έτσι υπάρχει μεγάλο coupling μεταξύ gui και κυρίως παιχνιδιού (btw, αν το ζητούμενο που κυριαρχεί είναι οι επιδόσεις, τότε και τα globals είναι προτιμητέα).

 

Βέβαια στο συγκεκριμένο παιχνίδι οι επιδόσεις δεν έχουν σημασία (τουλάχιστον δεν θα έπρεπε να έχουν) μιας και μιλάμε για κάτι πολύ απλό. Όταν ευκαιρήσω θα κάνω και το GameMemory opaque datatype και θα ποστάρω σχετικά σε αυτό το νήμα. Ενδεχομένως στο μίνι-api του να βάλω έναν set από νορμάλ constructor/destructor κι ένα  με set callbacks για user-defined allocator/deallocator.

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Λοιπόν το έκανα opaque το GameMemory, που είχε ως αποτέλεσμα και πολύ πιο clean κώδικα στο gui.

 

gui

 

 

/* win32_minesweeper.cpp */
...
int CALLBACK WinMain( ... )
{
	...
	if ( RegisterClass(&WindowClass) )
	{
		...
		if ( Window )
		{
			...
			/* εδώ */
			GameMemory *gamememory =
			game_memory_new( KILOBYTES(64), KILOBYTES(512) );
 
			_Win32GetBitmap24(
				DeviceContext,
				Instance,
				Win32SpriteSheetResourceID,
				/* εδώ */
				game_memory_get_trans( gamememory )
				);

			/* εδώ */
			if ( game_memory_has_perm_and_trans(gamememory) )
			{
				_g_Running = true;
				...
				while ( _g_Running )
				{
					...
					game_update_and_render(
						gamememory,
						...
					);

					...
				} // while
				...
				/* εδώ */
				game_memory_free( &gamememory );
			}
			...
		}
		...
	}
	...
}

 

4 αναφορές δηλαδή όλες κι όλες μέσα στο gui, μια και η ουσιαστική συμμετοχή του GameMemory είναι μέσα στην game_update_and_render(). Από interface, μόνο ένας constructor, ένας destructor, ένας getter κι ένας validator.

 

Και βασικά ο μόνος λόγος που βλέπω για να υπάρχει access στο GameMemory από το gui, είναι για να μπορεί να χρησιμοποιεί το εκάστοτε gui διαφορετικού μεγέθους sprites (και άρα να λέει στο κυρίως παιχνίδι πόση μνήμη πρέπει να δεσμευτεί αρχικά). Υπάρχει κάποιος άλλος που μου διέφυγε;

 

Επίσης, στον συγκεκριμένο κώδικα, το allocation γίνεται μια μόνο φορά, έξω από το game-loop, οπότε δεν βλέπω γιατί να πρέπει να ασχοληθούμε με custom allocator. Νομίζω πως αυτό το implementation/interface του GameMemory είναι λιτό, κατανοητό και αρκούντως αποτελεσματικό.

 

game.h (interface)

 

 

#ifndef GAME_H
#define GAME_H
...
typedef struct _GameMemory GameMemory;
...
extern GameMemory *game_memory_new( size_t permsize, size_t transsize );
extern void game_memory_free( GameMemory **memory );
extern void *game_memory_get_trans( const GameMemory *memory );
extern bool game_memory_has_perm_and_trans( const GameMemory *memory );
...
#endif

 

game.cpp (implementation)

 

 

/* game.cpp */
...
/* The game-memory is a contiguous piece, allocated in
 * heap memory, pointed to by *perm.
 *
 * However, permsize does NOT reflect the size of the
 * whole allocated piece, but its initial part to be
 * used as permanent storage.
 *
 * The remaining part is pointed to by *trans (which is
 * NOT malloced) and it equals to transsize bytes.
 *
 * Put otherwise, the total size of the allocated memory
 * equals to: permsize + transsize
 */
struct _GameMemory
{
	bool isInited;

	/* [NOTE]
	 * This stores the whole spritesheet at first
	 * and then the game board. Any leftover memory
	 * is used as a stack for the flood fill.
	 */
	void *trans;        // transient storage
	int  transsize;

	/* [NOTE]
	 * This stores the sprites.
	 */
	void *perm;         // permanent storage
	int  permsize;
};
...
/***********************************************//**
 * Mini-API for the opaque data type: GameMemory
 * (declared in game.h).
 ***************************************************
 */
struct _GameMemory *game_memory_new(
	size_t permsize,
	size_t transsize
	)
{
	/* sanity check */
	size_t size = permsize + transsize;
	if ( 0 == size ) {
		return NULL;
	}

	/* alloc main structure */
	struct _GameMemory *ret
	= (struct _GameMemory *) calloc( 1, sizeof(*ret) );
	if ( NULL == ret ) {
		return NULL;
	}

	/* alloc actual memory */
	if ( NULL == (ret->perm = calloc(size,1)) ) {
		free( ret );
		return NULL;
	}

	/* setup main structure */
	ret->permsize  = permsize;
	ret->trans     = (uint8_t *)(ret->perm) + permsize;
	ret->transsize = transsize;
	
	return ret;
}

void game_memory_free( struct _GameMemory **memory )
{
	if ( NULL != memory ) {
		free( (*memory)->perm );
		free( *memory );
		*memory = NULL;
	}
}

void *game_memory_get_trans( const struct _GameMemory *memory )
{
	if ( NULL == memory ) {
		return NULL;
	}
	return memory->trans;
}

bool game_memory_has_perm_and_trans( const struct _GameMemory *memory )
{
	return
	NULL != memory && NULL != memory->perm && NULL != memory->trans;
}

 

Αν θέλουμε να διατηρήσουμε το decoupling μεταξύ gui και κυρίως παιχνιδιού, και (για κάποιον λόγο) θέλουμε να παρέχουμε δυνατότητα για custom allocator/deallocator του game-memory, μπορούμε να προσθέσουμε στο interface από έναν έξτρα constructor και destructor, που θα δέχονται ως όρισμα αντίστοιχες callback ρουτίνες.

Π.χ...

 

game.h (interface)

 

 

#ifndef GAME_H
#define GAME_H
...
extern GameMemory *game_memory_new_custom(
			size_t permsize,
			size_t transsize,
			void *(*allocator)(void *userdata),
			void *userdata
			);
...
extern bool       game_memory_free_custom(
			GameMemory **memory,
			bool (*deallocator)(void *userdata),
			void *userdata
			);
...
#endif

 

game.cpp (implementation)

 

 

...
struct _GameMemory *game_memory_new_custom(
	size_t permsize,
	size_t transsize,
	void *(*allocator)(void *userdata),
	void *userdata
	)
{
	/* sanity check */
	size_t size = permsize + transsize;
	if ( 0 == size ) {
		return NULL;
	}
	/* alloc main structure */
	struct _GameMemory *ret
	= (struct _GameMemory *) calloc( 1, sizeof(*ret) );
	if ( NULL == ret ) {
		return NULL;
	}

	/* alloc actual memory */
	if ( NULL == (ret->perm = (*allocator)(userdata)) ) {
		free( ret );
		return NULL;
	}

	/* setup main structure */
	ret->permsize  = permsize;
	ret->trans     = (uint8_t *)(ret->perm) + permsize;
	ret->transsize = transsize;
	
	return ret;
}

bool game_memory_free_custom(
	struct _GameMemory **memory,
	bool   (*deallocator)( void *userdata ),
	void   *userdata
	)
{
	if ( NULL == memory ) {
		return false;
	}

	if ( !(*deallocator)(userdata) ) {
		return false;
	}

	free( *memory );
	*memory = NULL;
	return true;
}

 

Αλλά τώρα... bye-bye ο clean κώδικας στο gui:

 

 

 

/* win32_minesweeper.cpp*/
...
/* Datatype for win32-specific allocator */
struct _win32_game_memory_alloc_data {
    LPVOID lpAddress;        // address of region to reserve or commit  
    DWORD  dwSize;           // size of region 
    DWORD  flAllocationType; // type of allocation 
    DWORD  flProtect;        // type of access protection 
};

/* Datatype for win32-specific deallocator */
struct _win32_game_memory_dealloc_data {
    LPVOID lpAddress;       // address of region to reserve or commit  
    DWORD  dwSize;          // size of region 
    DWORD  dwFreeType;      // type of free operation 
};
...
/***********************************************//**
 *
 ***************************************************
 */
static inline void *CallbackGameMemoryCustomAlloc(
	void *cbdata
	)
{
	struct _win32_game_memory_alloc_data
	*data = (struct _win32_game_memory_alloc_data *)cbdata;

	return (void *) VirtualAlloc(
				data->lpAddress,
				data->dwSize,
				data->flAllocationType,
				data->flProtect
				);
}

/***********************************************//**
 *
 ***************************************************
 */
static inline bool CallbackGameMemoryCustomDealloc(
	void *cbdata
	)
{
	struct _win32_game_memory_dealloc_data
	*data = (struct _win32_game_memory_dealloc_data *) cbdata;

	return
	VirtualFree(
		data->lpAddress,
		data->dwSize,
		data->dwFreeType
		);
}

/***********************************************//**
 *
 ***************************************************
 */
int CALLBACK WinMain(...)
{
		...
		if ( Window )
		{
			...
			/* εδώ */
			struct _win32_game_memory_alloc_data
			gmad = {
				0,
				KILOBYTES(64) + KILOBYTES(512),
				MEM_RESERVE | MEM_COMMIT,
				PAGE_READWRITE
			};
			GameMemory *gamememory = game_memory_new_custom(
					KILOBYTES(64),
					KILOBYTES(512),
					CallbackGameMemoryCustomAlloc,
					&gmad
					);
			...
			/* εδώ */
			if ( game_memory_has_perm_and_trans(gamememory) )
			{
				_g_Running = true;
				...
				while ( _g_Running )
				{
					...
				} // while
				...
				/* εδώ */
				struct _win32_game_memory_dealloc_data
				gmdd = {
					(LPVOID) game_memory_get_perm(gamememory),
					0,
					MEM_RELEASE	
				};
				game_memory_free_custom(
					&gamememory,
					CallbackGameMemoryCustomDealloc,
					&gmdd
					);
			}
			...
	}
}

 

Και πιθανότατα με χειρότερο αποτέλεσμα, με τόσο overhead. Decoupling κι επιδόσεις συνήθως δεν πάνε μαζί, συνήθως πρόκειται για trade-off. Όμως με καλό σχεδιασμό εξαρχής μπορούν να επωφεληθούν και τα δυο (ελπίζω να μη φανταστεί κανείς ότι εγώ κάνω καλούς σχεδιασμούς εξαρχής... συνήθως πέφτει το re-factoring σύννεφο :P)

 

ΥΓ. Κώδικα δεν ανεβάζω ακόμα. Θα προσπαθήσω πρώτα να κάνω λίγο ξεκαθάρισμα στον σχεδιασμό και στην οργανωση (π.χ. στον διαχωρισμό αρχείων, datatypes, κλπ). Εν καιρώ όμως.

 

 

Τελείως άσχετο: Ο δοκιμαστικός δίσκος με τα 7-άρια τα 'παιξε. Βασικά μου το έκανε για 2η φορά.

 

Ξαφνικά μου τον βγάζει unbootable ο Intel manager. Την 1η φορά, τον έβγαλα (κούμπωσα τον παλιό με τα XP) και την επόμενη μέρα που τον δοκίμασα ξανά, δούλεψε μια χαρά... απλώς μου είπε πως δεν είχαν γίνει κανονικά shut-down τα Win-7 και μπήκα σε safe-mode απλώς για σιγουριά, και μετά όλα οκ).

 

Χτες μου το ξανα-έκανε. Τον έβγαλα (έβαλα τον παλιό με τα XP) και σήμερα που τον ξανα-δοκίμασα, μου είπε πάλι για abnormal shutdown, μπήκα safe-mode, μετά έκανα restart και κόλλησε στο logo των Windows 7.

 

Όταν τον συνδέω με ένα usb γκατζετακι ποθ έχω, δουλεύει μια χαρά, και το Crystal Disk Info (το SMART του δίσκου βασικά) μου τον δείχνει πεντακάθαρο (ούτε bad-sectors ούτε τίποτα).

 

Ξέρει κανείς τίποτα; (τώρα είμαι με τα XP πάλι σε αυτό το μηχανάκι)

 

  • Like 1
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Κοίταγα λίγο πάλι τον κώδικα, και τελικά έχω fundamental απορίες. Βασικά, δυσκολεύομαι να καταλάβω ποια data πρέπει (θέλουμε) να ανήκουν στο core game και ποια στο gui. Χωρίς να είναι ξεκάθαρο αυτό, είναι δύσκολο να σχεδιάσουμε και το πως θα επικοινωνούν μεταξύ τους το core-game με το gui (ή με άλλα λόγια, με ποιον τρόπο θα ανταλλάσσουν data και ποια data).

 

Ένα παράδειγμα το έθιξα ήδη σε προηγούμενο ποστ. Έχοντας τον αρχικό κώδικα, πως μπορεί το gui να πάρει πληροφορίες όπως το μέγεθος του ταμπλό ή το πλήθος των ναρκών, από το core-game; Έδωσα μια πιθανή λύση εγώ, κάνοντας το GameOptions accessible ως opaque type στο gui, αλλά αν δεν κάνω λάθος, ο αρχικός κώδικας δεν δίνει τέτοια πληροφόρηση στο gui, σωστά;

 

Ένα άλλο παράδειγμα, το GameUi πρέπει να το διαχειρίζεται το core-game ή το gui, και γιατί. Όπως είναι τώρα ο κώδικας, νομίζω πως το GameUi διαχειρίζεται αποκλειστικά από το core-game, μιας και παρόλο που η UI είναι καθολική μεταβλητή, το gui δεν ασχολείται καθόλου μαζί της (δηλαδή η καθολικότητα της UI δείχνει να είναι intended μονάχα για τα πηγαία αρχεία του core-games, παρόλο που από ότι είδα τελικά, πληροφορίες σχετικά με το μέγεθος του ταμπλό, το πλήθος των ναρκών, κλπ μπορούν τα τραβηχτούν από την UI... άρα δεν χρειαζόταν να κάνω expose το GameOptions).

 

Γενικώς, χωρίς κάποιου είδους documentation, είτε στον κώδικα είτε εδώ στο νήμα, τελικά το βλέπω δύσκολο να έχω την υπομονή να αποκρυπτογραφήσω την σχεδιαστική λογική του (ίσως να παίζει ρόλο πως ασχολούμαι τμηματικά/περιστασιακά, αλλά και πάλι). Αντίθετα, αν υπάρξει μια περιληπτική έστω εξήγηση, θα διευκολύνει πολύ στο να ασχοληθώ κι εγώ (υποθέτω και οποιοσδήποτε άλλος) για port του gui και σε άλλες πλατφόρμες, και ευδιάκριτο διαχωρισμό μεταξύ core & gui (καθώς και να αποφύγουμε διπλο-δουλειές).

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

migf1 - Από ότι φαινεται τα πράγματα που μου φαινονται προφανή επειδη το έγραψα δεν είναι και τοσο προφανη τελικα :P Πρέπει να μάθω να βάζω λίγο περισσότερα σχόλια σε κάποια πράγματα.

 

Το windows layer(ή gui οπως το λες) καλεί το παιχνίδι με την GameUpdateAndRender. Το winlayer χρειάζεται να γνωρίζει τα struct που περνάει με την συνάρτηση αυτή στο παιχνίδι. Δεν υπάρχει λόγος να ξέρει κάτι άλλο.

 

Η δουλειά του windows layer είναι να διαβάζει το input από τον χρήστη και να σχεδιάζει έναν buffer σε ένα παράθυρο. Τα περιεχόμενα του buffer τα αποφασίζει το παιχνίδι με βάση το input, οπότε αυτά θα περάσει το winlayer στο παιχνίδι πριν σχεδιάσει το αποτέλεσμα.

 

Τέλος, το winlayer παρέχει στο παιχνίδι και το game_memory για την μνήμη που μπορει να χρησιμοποιήσει εκτός του buffer. Αυτό γίνεται επειδή αποφάσισα να γίνεται η αρχικοποιηση της μνήμης στο windows layer με windows specific τρόπο, αλλά μπορεί να γίνει και στο core-game με *alloc την πρώτη φορα που καλείται η GameUpdateAndRender. (+ υπάρχει και το θέμα του spritesheet που οπως είναι τώρα το διαβάζει το windows layer και το κοτσάρει στο game_memory την πρώτη φορά που το αρχικοποιεί, θα πρέπει να αλλάξει λίγο αυτο αν μεταφερθεί)

 

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

 

Ευχαριστώ πάντως που ασχολείσαι, παρόλο που από οτι τελικά φαίνετια σε έχω δυσκολέψει αρκετά :P

  • Like 1
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

migf1 - Από ότι φαινεται τα πράγματα που μου φαινονται προφανή επειδη το έγραψα δεν είναι και τοσο προφανη τελικα :P Πρέπει να μάθω να βάζω λίγο περισσότερα σχόλια σε κάποια πράγματα.

Πολλές φορές βλέπουμε να γράφεται κώδικας για το Χ βασικό πράγμα αντί να χρησιμοποιηθεί η πάρα πολύ γνωστή βιβλιοθήκη Ψ που το υλοποιεί ήδη. Είναι πολύ πιο εύκολο να γράψεις κώδικα παρά να διαβάσεις κώδικα (ειδικά που τον έγραψε κάποιος άλλος με το δικό του προσωπικό στυλ).

 

Ευχαριστώ πάντως που ασχολείσαι, παρόλο που από οτι τελικά φαίνετια σε έχω δυσκολέψει αρκετά :P

Δεν παθαίνει τίποτα. Καλά κάνεις και τον παιδεύεις :) Εγώ δεν ασχολήθηκα όχι γιατί σε έγραψα αλλά γιατί ο κώδικας δεν γινόταν compile λόγω windows.h και λοιπών api και μου είναι λίγο αγγαρεία να σηκώσω VM και να δουλέψω σε windows :)
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Σε ευχαριστώ για την άμεση απάντηση!

 

migf1 - Από ότι φαινεται τα πράγματα που μου φαινονται προφανή επειδη το έγραψα δεν είναι και τοσο προφανη τελικα :P Πρέπει να μάθω να βάζω λίγο περισσότερα σχόλια σε κάποια πράγματα.

Δεν είσαι ο μόνος. Όλοι την πατάμε έτσι ;)

 

Το windows layer(ή gui οπως το λες) καλεί το παιχνίδι με την GameUpdateAndRender. Το winlayer χρειάζεται να γνωρίζει τα struct που περνάει με την συνάρτηση αυτή στο παιχνίδι. Δεν υπάρχει λόγος να ξέρει κάτι άλλο.

Άρα λοιπόν τα GameMemory, GameBackBuffer, PrevGameInput και CurrGameInput.

	GameUpdateAndRender(
		&GameMemory,
		&GameBackBuffer,
		&PrevGameInput,
		&CurrGameInput
		);

Η δουλειά του windows layer είναι να διαβάζει το input από τον χρήστη και να σχεδιάζει έναν buffer σε ένα παράθυρο. Τα περιεχόμενα του buffer τα αποφασίζει το παιχνίδι με βάση το input, οπότε αυτά θα περάσει το winlayer στο παιχνίδι πριν σχεδιάσει το αποτέλεσμα.

Μέχρι εδώ έχω λοιπόν την 1η απορία, πως μπορεί το winlayer (gui) να τραβήξει από το core-game πληροφορίες όπως π.χ. το μέγεθος του ταμπλό και το πλήθος των ναρκών (για να τα τυπώσει ας πούμε στον τίτλο του παραθύρου του); Πες πως το μέγεθος του ταμπλό το παίρνει από την καθολική UI, το πλήθος των ναρκών όμως;

 

Θα μου πεις το πλήθος των ναρκών το εμφανίζει το core-game, αλλά πες για παράδειγμα πως θέλουμε στο GUI να ρωτάμε τον χρήστη πόσες νάρκες θέλει να έχει το ταμπλό;

 

Ή να το πω αλλιώς, πες πως θέλουμε να προσθέσουμε επίπεδα δυσκολίας, όπου π.χ. το easy θα γεμιζει το 25% του ταμπλό με νάρκες, το medium θα γεμίζει το 50% και το hard θα γεμίζει το 75%. Βάζουμε λοιπόν στο gui ένα σχετικό μενού/dialog/οτιδήποτε για να διαλέξει ο χρήστης επίπεδο δυσκολίας. Για παράδειγμα, όπως είναι εδώ ή εδώ το Game menu. Πώς θα περάσουμε από το gui στο core-game το πλήθος των ναρκών; Ακόμα "χειρότερα" αν θέλουμε να κάνουμε implement στο GUI και το Options menu του 2ου link.

 

Εννοώ δηλαδή πως από όσο έχω καταλάβει, ο κώδικας δεν είναι flexible σε ότι αφορά το decoupling μεταξύ core-game και gui.

 

Το βρίσκω ενδιαφέρον το core-game να φτιαχτεί έτσι ώστε να κάνει expose ένα api με όσες πληροφορίες θέλουμε να είναι παραμετροποιήσιμες KAI εξωτερικά. Ο κώδικάς σου, ενώ είναι ανεπτυγμένος με παραμετροποιήσιμη λογική στο εσωτερικό του core-game, το μεγάλο ποσοστό αυτής της παραμετροποίησης είναι σχεδόν τελείως αποκομμένο από άλλα modules (όπως το gui) και είναι κρίμα. Εγώ θέλω να επιχειρήσω να του δώσω αυτό το flexibility, αλλά σε κάποια σημεία ψιλο-χοντρο-χάνομαι, γιατί στον υπάρχοντα κώδικα δυσκολεύομαι να εντοπίσω τις πληροφορίες που θέλω. Είναι οργανωμένες με άλλη λογική και μερικές βρίσκονται σε περισσότερα του ενός struct.

 

Τέλος, το winlayer παρέχει στο παιχνίδι και το game_memory για την μνήμη που μπορει να χρησιμοποιήσει εκτός του buffer. Αυτό γίνεται επειδή αποφάσισα να γίνεται η αρχικοποιηση της μνήμης στο windows layer με windows specific τρόπο, αλλά μπορεί να γίνει και στο core-game με *alloc την πρώτη φορα που καλείται η GameUpdateAndRender. (+ υπάρχει και το θέμα του spritesheet που οπως είναι τώρα το διαβάζει το windows layer και το κοτσάρει στο game_memory την πρώτη φορά που το αρχικοποιεί, θα πρέπει να αλλάξει λίγο αυτο αν μεταφερθεί)

Θα μπορούσες κάποια στιγμή να εξηγήσεις τι ακριβώς κάνεις με το game-memory και γιατί (π.χ. τι ακριβώς κάνεις με το perm και τι με το trans); Στο gui βλέπω πως διαχειρίζεσαι μονάχα το trans, οπότε όπως είναι τώρα δεν βλέπω τον λόγο να γίνεται exposed και το perm. Αυτό π.χ. το άλλαξα όταν έκανα opaque το GameMemory (όπως εξήγησα στο προηγούμενο ποστ)... έβαλα getter μονάχα για τον trans, παρόλο που ο constructor θέλει size και για τον perm. Αυτό ίσως είναι λογική ασυνέπεια, αλλά δεν έχω πολυκαταλάβει τι προσπαθείς να κάνεις με τα perm και trans.

 

Εξηγείς με σχόλια πως ο perm δείχνει στα sprites, ενώ ο trans αρχικά στο spritesheet και μετά στο board, αλλά δεν εξηγείς ποια είναι η μεταξύ τους σχέση... οκ sprite & spritesheet έχουν προφανή σχέση (το εκάστοτε sprite περιέχεται μέσα στο spritesheet) αλλά το board πως το συνδυάζεις με τα υπόλοιπα; Και τι ακριβώς εννοείς πως στην αρχή φορτώνεις το spritsheet στον trans, αλλά μετά φορτώνεις εκεί το board. Μήπως ως "sprites" εννοείς αυτό που σε όλον τον υπόλοιπο κώδικα ονομάζεις "spritemap", κι εννοείς πως αρχικά φορτώνεις το spritesheet, τραβάς όλα τα sprites μέσα στον spritemap και άρα δεν θες άλλο το spritesheet; Δηλαδή, το perm + permsize είναι ουσιαστικά ο spritemap? Γενικώς, χρειάζονται εξηγήσεις :P

 

Btw, εμένα μου φαίνεται λογικό το gui να αποφασίζει πόση μνήμη θα χρησιμοποιηθεί για τα sprites, να την αιτείται και κατόπιν να περνάει μέσω αυτής τα sprites στο core-game. Διότι το πόσο μεγάλα θα είναι τα sprites, καθώς και από ποιο bitmap θα φορτώνονται δεν είναι δουλειά του core-game. Το core-game μπορεί να θέτει κάποιες προδιαγραφές (π.χ. τα θέλω σε ένα array, πρώτα για τα νούμερα, μετά για τις νάρκες/σημαίες/ερωτηματικά, κλπ, με ίσες διαστάσεις ανά κατηγορία & πες μου ποιες είναι αυτές για κάθε κατηγορία) και το gui να τα φορτώνει/φτιάχνει και να τα περνάει στη μορφή που θέλει το core-game. Θέλω να πω, καλώς για μένα το gui καθορίζει τη απαιτούμενη μνήμη. Αυτό που είπα στο προηγούμενο πόστ είναι πως δεν βρίσκω τον λόγο για το συγκεκριμένο game ο allocator μην είναι cross-platform (δηλαδή το βρίσκω πιο sane να παρέχεται από το core-game, αλλά η κλήση του να παραμείνει στο gui).

 

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

 

Ευχαριστώ πάντως που ασχολείσαι, παρόλο που από οτι τελικά φαίνετια σε έχω δυσκολέψει αρκετά :P

Είναι μια καλή αρχή αυτές οι εξηγήσεις. Θέλω κι άλλες όμως όπως είδες :lol: (εννοείται όποτε μπορέσεις).

 

EDIT:

 

...

Δεν παθαίνει τίποτα. Καλά κάνεις και τον παιδεύεις :) Εγώ δεν ασχολήθηκα όχι γιατί σε έγραψα αλλά γιατί ο κώδικας δεν γινόταν compile λόγω windows.h και λοιπών api και μου είναι λίγο αγγαρεία να σηκώσω VM και να δουλέψω σε windows :)

Το Wine (με ή χωρίς mingw crosscompile) δεν βολεύει;

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Γενικά θα προτιμούσα οι ρυθμίσεις του παιχνιδιού να γίνονται ingame και όχι με windows dialogs menus (για αυτό υλοποιησα στα γρήγορα το περιεργο σύστημα με τα βελάκια κτλ αντί για ένα μενού με επιλογές)

 

Επίσης σχετικά με το "χαζό" παράδειγμα - αν θέλει να τυπώσει τις νάρκες στον τίτλο του παράθυρου. Κατα την γνώμη μου αν χρειαζόταν το παιχνίδι να έχει τέτοιου είδους έλεγχο πιστευω ότι θα έπρεπε να δίνει πίσω στο winlayer ένα windowtitle και όχι το winlayer να πρέπει να ξέρει ότι υπάρχει κάτι που λέγεται νάρκες και θέλω να φαινεται στον τίτλο.

 

Κυρίως αυτή τη λογική ακολούθησα - νομίζω έτσι είναι ευκολότερο στο port καθώς το μονο που χρειάζεται αυτός που θα το κάνει είναι να ξερει οτι δινω στο παιχνιδι συγκεκριμένα αόριστα πράγματα(εναν buffer, input, memory κτλ.)

 

Δυστυχώς αυτή η (ωραια κατα την γνώμη μου) λογική χαλάει αν πάμε να βάλουμε options σε ενα windows dialog πχ.

 

Περί game_memory:

 

Την μνήμη του παιχνιδιού την έχω χωρίσει σε δυο κομμάτια : την "μόνιμη" και την "παροδική".

Στην μόνιμη αποθηκεύονται πράγματα που από την αρχή μέχρι το τέλος του παιχνιδιού δεν θα αλλάξουν (τα κομμένα sprites από το spritesheet).

Στην παροδική αποθηκεύονται όλα τα υπόλοιπα. Αρχικά εκεί βάζω όλο το spritesheet από το οποίο το παιχνίδι κοβει τα sprites και τα βάζει στη "μονιμη" μνήμη. Το spritemap τελικά είναι ένα σύνολο δεικτών για να βρίσκεις μέσα στη "μονιμη" μνήμη το κάθε ένα game_sprite. Απο κει και πέρα ο χώρος της "παροδικής" μνήμης χρησιμοποιείται για την αποθήκευση του board και οτι περισσεύει για στοίβα για το flood fill.

 

Το winlayer αυτό που κάνει είναι να αρχικοποιεί ένα block και για τα δύο, και στην συνέχεια να προσαρμώζει τους δείκτες στις αρχές του κάθε partition.

 

Δηλαδή γίνεται το εξής:

1) Μόνιμη:64ΚΒ, Παροδική:512ΚΒ -> Σύνολο 512+64=578ΚΒ.

2) Δεσμεύουμε λοιπόν 578ΚΒ μνήμης.

3) Την αρχή της μνήμης που δεσμέυσαμε την βάζουμε στο δείκτη που θέλουμε να δείχνει την "μονιμη" μνήμη. Εφόσον έχουμε το perm_size ξέρουμε τα όρια της και δεν(αν το προσέξουμε) θα γράψουμε κατα λάθος μετά από αυτό.

4) Υπολογίζουμε και την θέση του δείκτη που θέλουμε να δείχνει στην "παροδική" μνήμη. (Οπου δειχνει ο perm + 64KB)

 

Φυσικά θα μπορούσε να δεσμεύσει απλά το block και να φτιάχνει τους δείκτες το παιχνίδι. Όμως νομίζω θα πολυπλοκεύσει ο κώδικας χωρίς λογο(που θα ξέρει το winlayer πόση μνήμη θέλει το παιχνίδι ή ανάποδα που θα ξέρει το παιχνίδι που τελειώνει το perm για να φτιάξει τους δείκτες.)

  • Like 1
Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

Γενικά θα προτιμούσα οι ρυθμίσεις του παιχνιδιού να γίνονται ingame και όχι με windows dialogs menus (για αυτό υλοποιησα στα γρήγορα το περιεργο σύστημα με τα βελάκια κτλ αντί για ένα μενού με επιλογές)

 

Επίσης σχετικά με το "χαζό" παράδειγμα - αν θέλει να τυπώσει τις νάρκες στον τίτλο του παράθυρου. Κατα την γνώμη μου αν χρειαζόταν το παιχνίδι να έχει τέτοιου είδους έλεγχο πιστευω ότι θα έπρεπε να δίνει πίσω στο winlayer ένα windowtitle και όχι το winlayer να πρέπει να ξέρει ότι υπάρχει κάτι που λέγεται νάρκες και θέλω να φαινεται στον τίτλο.

 

Κυρίως αυτή τη λογική ακολούθησα - νομίζω έτσι είναι ευκολότερο στο port καθώς το μονο που χρειάζεται αυτός που θα το κάνει είναι να ξερει οτι δινω στο παιχνιδι συγκεκριμένα αόριστα πράγματα(εναν buffer, input, memory κτλ.)

 

Δυστυχώς αυτή η (ωραια κατα την γνώμη μου) λογική χαλάει αν πάμε να βάλουμε options σε ενα windows dialog πχ.

Βασικά με αυτή την λογική ανάπτυξης σχεδόν καταργείς τον βασικό ρόλο ύπαρξης GUI (που όπως λέει και το όνομά του είναι η αλληλεπίδραση με τον χρήστη) και το χρησιμοποιείς κυρίως ως έναν απλό καμβά, για να οπτικοποιήσεις τα περιεχόμενα του backbuffer.

 

To χρησιμοποιείς όμως για να πάρεις και το input, το οποίο πολύ σωστά το φέρνεις πρώτα στη μορφή που το θέλει το core-game (δηλαδή σε μορφή struct GameRawInput) και μετά το στέλνεις στο core-game από το gui. Βέβαια αυτή την μετατροπή την κάνεις με γνώση της εσωτερικής υλοποίησης του GameRawInput, κάτι που έχει πολύ υψηλό coupling και σημαίνει πως αλλάζοντας την υλοποίηση στο core-game θα πρέπει να αλλάξει και ο κώδικας στο gui. Θα ήταν πιο σωστό το gui να μην ξέρει την εσωτερική υλοποίηση, και απλώς να χρησιμοποιεί το GameRawInput ως opaque type, μέσω συναρτήσεων που θα παρέχει το core-game.

 

Αλλά το βασικό point είναι το 1ο. Ότι δηλαδή στο gui μετατρέπεις το input στην μορφή που περιμένει το core-game. Το ίδιο πράγμα σου πρότεινα κι εγώ να κάνεις και με τα υπόλοιπα παραμετροποιήσιμα στοιχεία του core-game, αλλιώς το decoupling με το gui δεν έχει και πολύ μεγάλο νόημα.

 

Το core-game θέτει κάποιες προδιαγραφές (αλλού πιο συγκεκριμένες αλλού πιο ελαστικές) για τα data που θέλει και το gui του τα στέλνει. Το core-game τα επεξεργάζεται και στέλνει πίσω στο gui τα επεξεργασμένα data προς εμφάνιση. Το ποια data είναι ελαστικά και ποια όχι, μπορεί να αλλάζει από version σε version, αλλά το challenge είναι το core-game να φτιαχτεί έτσι ώστε αφενός να είναι εύκολο να προστεθούν νέα data στα διαθέσιμα προς τα έξω (και προς τα μέσα), κι αφετέρου οι αλλαγές αυτές να μην σπάνε υπάρχοντα εξωγενή κώδικα.

 

Απλά data όπως είναι το board-size και το πλήθος των ναρκών, είναι πολύ πιο απλά από το input που ήδη το κάνεις ;)

Αλλά θέλει να οργανωθεί λίγο διαφορετικά ο κώδικας (structs, files, κλπ).

 

Το να φτιαχτεί τώρα όπως είναι π.χ. ένα gtk_mainsweeper.cpp που θα είναι ένας απλός καμβάς με λίγο input sending, δεν είναι και πολύ intriguing :)

 

Περί game_memory:

 

Την μνήμη του παιχνιδιού την έχω χωρίσει σε δυο κομμάτια : την "μόνιμη" και την "παροδική".

Στην μόνιμη αποθηκεύονται πράγματα που από την αρχή μέχρι το τέλος του παιχνιδιού δεν θα αλλάξουν (τα κομμένα sprites από το spritesheet).

Στην παροδική αποθηκεύονται όλα τα υπόλοιπα. Αρχικά εκεί βάζω όλο το spritesheet από το οποίο το παιχνίδι κοβει τα sprites και τα βάζει στη "μονιμη" μνήμη. Το spritemap τελικά είναι ένα σύνολο δεικτών για να βρίσκεις μέσα στη "μονιμη" μνήμη το κάθε ένα game_sprite. Απο κει και πέρα ο χώρος της "παροδικής" μνήμης χρησιμοποιείται για την αποθήκευση του board και οτι περισσεύει για στοίβα για το flood fill.

 

Το winlayer αυτό που κάνει είναι να αρχικοποιεί ένα block και για τα δύο, και στην συνέχεια να προσαρμώζει τους δείκτες στις αρχές του κάθε partition.

 

Δηλαδή γίνεται το εξής:

1) Μόνιμη:64ΚΒ, Παροδική:512ΚΒ -> Σύνολο 512+64=578ΚΒ.

2) Δεσμεύουμε λοιπόν 578ΚΒ μνήμης.

3) Την αρχή της μνήμης που δεσμέυσαμε την βάζουμε στο δείκτη που θέλουμε να δείχνει την "μονιμη" μνήμη. Εφόσον έχουμε το perm_size ξέρουμε τα όρια της και δεν(αν το προσέξουμε) θα γράψουμε κατα λάθος μετά από αυτό.

4) Υπολογίζουμε και την θέση του δείκτη που θέλουμε να δείχνει στην "παροδική" μνήμη. (Οπου δειχνει ο perm + 64KB)

Thanks, αν και μέχρι εκεί το είχα κάνει τελικά κι εγώ figure-out. Οι απορίες μου αφορούν πιο εξειδικευμένα στοιχεία. Όπως π.χ. τελικά η PermanentStorage και το SpriteMap είναι το ίδιο και το αυτό; Όταν λες πως στην PermanentStorage αποθηκεύεις τα κομμένα (από το spritesheet) sprites, σε τι μορφή τα αποθηκεύεις. Με άλλα λόγια, γιατί υπάρχει και PermanentStorage και SpriteMap[] στον κώδικα, αν και τα 2 αυτά αποθηκεύουν μεμονωμένα sprites?

(btw, άσχετο με τα παραπάνω, αλλά επειδή δεν το έχω κοιτάξει, τα sprites τα έχεις κάνεις να μπορούν να είναι αλλάζουν διαστάσεις, ή τις έχεις fixed σύμφωνα με το τρέχον spritesheet? )

 

Φυσικά θα μπορούσε να δεσμεύσει απλά το block και να φτιάχνει τους δείκτες το παιχνίδι. Όμως νομίζω θα πολυπλοκεύσει ο κώδικας χωρίς λογο(που θα ξέρει το winlayer πόση μνήμη θέλει το παιχνίδι ή ανάποδα που θα ξέρει το παιχνίδι που τελειώνει το perm για να φτιάξει τους δείκτες.)

Σε έχασα εδώ :(

Συνδέστε για να σχολιάσετε
Κοινοποίηση σε άλλες σελίδες

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

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

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

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

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

Σύνδεση

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

Συνδεθείτε τώρα

  • Δημιουργία νέου...