Terve
Millainen viritelmä kannattaisi tehdä, jos haluan kaksi samanlaista Java-ohjelmaa kommunikoimaan netin yli keskenään? Pitääkö aina olla server-client-tyylinen ohjelma?
Ohjelman toimisi seuraavasti.
1. Toinen valitsee listasta IP-osoitteen jonka kanssa haluaa avata yhteyden.
2. Kohteessa kysytään, haluaako käyttäjä avata yhteyden.
3. Hyväksynnän jälkeen molemmat pystyvät lähettämään ja vastaanottamaan dataa.
Ohjelman pitäisi toimia niin, että molemmat pystyvät halutessa avaamaan yhteyden tai vastaanottamaan sen, ei niin, että vain toinen pystyisi olemaan se palvelin- tai asiakasohjelma.
Pakkohan sen kohdekoneen on jollain tavalla olla valmiina, jotta se pystyy vastaanottamaan yhteyden. Siinä varmaan täyttyy tuo palvelimen määritelmä. Luotettavassa datan siirrossa tarvitset siis kuuntelevan TCP-socketin (ServerSocket). Yhteyden muodostamisen jälkeen kuitenkin molemmissa päissä on käytössä tavallinen Socket eikä ole enää mitään väliä, kumpi aluksi oli se yhdistävä osapuoli.
Jos epävarma UDP riittää, voit avata yhden socketin (DatagramSocket) ja vastaanottaa paketteja (DatagramPacket), joista selviää sitten vastaanottamisen jälkeen lähettäjän osoite, jonka perusteella voit katsoa, onko yhteys jo auki tai onko osoite jo päätetty estää kokonaan.
Muista palomuurikysymykset. Useissa tapauksissa NAT tai palomuuri estää TCP-porttiin yhdistämisen täysin, kun taas UDP:n kanssa voidaan yleensä hyödyntää hole punching -kikkaa, jossa yhdistämiseen tarvitaan julkisen palvelimen apua.
Toisellahan on pakko olla jo se ohjelma auki, eikö niin?
Eli aina kun ohjelma avataan, pistä se kuuntelemaan sovittua porttia. (tätä voi nimittää nyt sitten vaikka serveriksi jos haluaa) Ja sitten taas tämä toinen joka valitsee ip osoitteen ja klikkaa yhteyden, on tässä tapauksessa client. Ei siinä sen kummempaa.
Itse ohjelmahan voi olla molemmilla prikulleen sama.
Ajattelinkin käynnistää sen kuuntelevan sokketin taustalle (toiseen säikeeseen) ohjelman käynnistyessä. Jos molemmilla on jo käynnissä ServerSocket, niin pitääkö se sammuttaa tältä asiakkaalta ennen kuin voi yhdistää palvelimeen?
Jos haluan, että ohjelmassa näkyy mitkä listan koneista ovat käynnistäneet ohjelmansa, niin milläs tämä tapahtuu? Tämä yksi ohjelma ei ole koko ajan käynnissä, joten toisen käynnistyessä se ei vain voi lähettää sille tietoa siitä. Jokaisen käynnistyvän ohjelman pitäisi koittaa sokkettia jokaiseen listan koneista, ja ne jotka vastaavat ovat paikalla, eikö?
Saan aikaan vain versioita, joissa kaikki ohjelmat ovat asiakkaita ja yksi tietokone palvelisi näitä kaikkia. Sille lähetettäisiin viesti kun ohjelma käynnistyy ja kun se sammutetaan. Näin se mielestäni olisi helpoin, mutta se ei onnistu. Ohjelman pitää olla yksikseen (stand alone).
Jos kumminkin vielä ajatellaan asiaa siten, ettei toisien ohjelmien tarvitse tietää onko toinen päällä. Vinkkejä saa kumminkin antaa. Hienosäädöt vasta lopuksi.
Edit. Miten muuten käy, jos halutaan että moni käyttäjä voi yhdistää samaan serveriin? Tarkoitus olisi liittää 2-4 käyttäjää samaan chattiin.
Muutin ohjelman rakennetta niin, että ensin pitää jommankumman pitää käynnistää itselleen palvelinpuoli ja sen jälkeen voi yhdistää.
Tein kaksi luokkaa tämän avulla.
//ServerSocketListener.java package ohjelma; import java.io.*; import java.net.*; import javax.swing.JOptionPane; public class ServerSocketListener extends Thread { @Override public void run() { ServerSocket serverSocket = null; boolean listening = true; try { serverSocket = new ServerSocket(7777); } catch(IOException e) { JOptionPane.showMessageDialog(null, "ServerSocketListener(): " + e, "Virhe!", JOptionPane.ERROR_MESSAGE); } while(listening) { try { new MultiServerThread(serverSocket.accept()).start(); } catch (IOException ex) { System.out.println("Listening: " + ex); } } try { serverSocket.close(); } catch (IOException ex) { System.out.println("close(): " + ex); } } }
//MultiServerThread.java package ohjelma; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; public class MultiServerThread extends Thread { private Socket socket = null; public MultiServerThread(Socket socket) { super("MultiServerThread"); this.socket = socket; System.out.println("Client connected."); System.out.println(socket.getInetAddress()); } @Override public void run() { try { PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); String inputLine, outputLine; out.println("Huomenta"); while((inputLine = in.readLine()) != null) { outputLine = "Viestisi: " + inputLine; out.println(outputLine); if(outputLine.equals("Bye")) break; } out.close(); in.close(); socket.close(); } catch(IOException e) { System.out.println("Virhe MultiServerThread.java: " + e); } } }
Nämä toimivat ihan hienosti, moni voi ottaa yhteyden ja yhteydet pelaavat: serveri voi lähettää kaikille dataa ja kaikki serverille. Mutta mites jos halutaan, että kaikki serverille/serveriltä lähetetty data näkyy kaikilla käyttäjillä? Pitäisikö serverin välittää se kaikille?
Olisiko tähän jotain helppoa tapaa toteuttaa metodia, joka lähettää jokaiselle saman datan? Pitäisikö MultiServerThreadissa luoda erikseen lähetysmetodi ja ServerSocketListenerissä tallentaa threadit taulukkoon josta voidaan kutsua lähetysmetodia MultiServerThreadista? Ja koska data ei tule ServerSocketListeneriltä, vaan se tulee pääohjelmalta, niin ServerSocketListeneriinkin pitäisi luoda metodi viestin lähetystä varten, jota kutsutaan pääohjelmasta ja se välittää muuttujan sisällön MultiServerThreadiin?
Entäs äskeinen toisinpäin? Data vastaanotetaan asiakkailta MultiServerThreadissa, miten se välitetään takaisin pääohjelmalle? MultiServerThread on luotu ServerSocketListeneristä joka taas on luotu pääohjelmasta X.java.
Macro kirjoitti:
Mutta mites jos halutaan, että kaikki serverille/serveriltä lähetetty data näkyy kaikilla käyttäjillä? Pitäisikö serverin välittää se kaikille?
Pitäisi. Ja ihan mielekkään ratkaisunkin keksit. Miksi edes kyselet, kun homma selvästi ratkeaa sillä, että säilytät tarpeelliset tiedot (eli listan asiakkaista) sopivassa paikassa ja lisäilet tarvittavia funktioita datan siirtoon? Muista vain käsitellä myös yhteyden katkeaminen.
ServerSocketListener-koodissasi muuten kannattaisi ensimmäisessä catch-lohkossa olla return, ettei kuuntelu turhaan yritä jatkua. Toisaalta olisi fiksua laittaa socketin luonti jo luokan konstruktoriin ja ottaa poikkeus kiinni kutsupaikalla (eli new ServerSocketListener() -rivin ympärillä käyttöliittymän puolella). On hyvien tapojen vastaista sekoittaa palvelinkoodiin käyttöliittymäkoodia.
Kiitos vinkistä.
Kyselen jos löytäisin parempia ratkaisuja, koska välillä tapani vaikuttavat niin monimutkaisilta ja raskailta
Mutta miten mahtaa hoitua vastaanotetun datan käsittely pääohjelmassa? Toiseen suuntaan keksinkin tavan liikuttaa tavaraa, mutta näinpäin saan vain virheen (non-static method/variable cannot be referenced from a static context).
Sinun pitää välittää pääluokan olio noille toisille luokille. Tosin sitä ei tietenkään ole siistiä välittää sellaisenaan, vaan parempi on tehdä rajapinta tai käyttää viestien välitykseen täysin erillistä luokkaa (kuten jonoa, johon työnnät viestejä ja jota luet pääohjelman silmukassa).
Kirjoitin nyt tällaisen pienen mallikoodin, jossa käytetään rajapintoja viestien välitykseen. Koodista puuttuu kaikenlaista (mm. poikkeusten käsittelyä), mutta siinä on pari muutakin kehitysideaa versioosi, erityisesti yhtä asiakasta kuvaavan luokan siirtäminen palvelimen alle ja synchronized-sanan käyttö yhtäaikaisuuksien välttämiseksi.
interface Connection { public void sendMessage(String data); }
interface MessageHandler { // Metodi, jolla saapuneet viestit ilmoitetaan. Parametreina ovat oliot, // joilla voi lähettää viestejä kaikille tai vastata vain lähettäjälle. public void handleMessage(Connection group, Connection client, String data); }
public class Ohjelma implements MessageHandler { private void listen() { try { new Server(this); } catch (Exception e) { JOptionPane.showMessageDialog(null, e.toString(), "Virhe!", JOptionPane.ERROR_MESSAGE); } } public void handleMessage(Connection group, Connection client, String data) { if (data.equals("yksityisviesti")) { client.sendMessage("ok"); } else { group.sendMessage("tiedoksi kaikille: " + data); } } }
public class Server implements Connection { private final MessageHandler handler; private final List<ClientThread> clients = new ArrayList<ClientThread>(); public Server(MessageHandler handler) { this.handler = handler; } public synchronized void sendMessage(String msg) { synchronized(clients) { for (ClientThread c: clients) { c.sendMessage(msg); } } } private class ClientThread extends Thread implements Connection { private final Socket s; private final PrintStream output; private final Scanner input; public ClientThread(Socket s) { this.s = s; output = new PrintStream(s.getOutputStream()); input = new Scanner(s.getInputStream()); } // Metodi yhteyden sulkemista varten. public void interrupt() { s.close(); super.interrupt(); } // Viestin lähetys. public synchronized void sendMessage(String msg) { output.println(msg); output.flush(); } // Viestejä vastaanottava silmukka; alussa ja lopussa // yksilö osaa lisätä ja poistaa itsensä listalta. public void run() { synchronized(clients) { clients.add(this); } // Silmukassa varaudutaan myös yhteyden katkaisuun molemmin puolin. try { while (!isInterrupted() && input.hasNextLine()) { handler.handleMessage(Server.this, this, input.nextLine()); } } catch (Exception e) { } synchronized(clients) { clients.remove(this); } } } }
Mitä etuja sinun tapasi tehdä tämä tuo siihen verrattuna, että kuljettaisi tarvittavia muuttujia metodien mukana?
Mitä kohtaa erityisesti tarkoitat? Tietenkin voit tehdä, miten parhaaksi näet. Uskon kuitenkin, että koodista tulisi tällä tavalla yksinkertaisempi kuin vaihtoehdosta, jossa handler ja clients (ja mikähän muu vielä?) kulkisivat parametreina ympäriinsä. Muunlaisia etuja harvoin kannattaa hakeakaan, koska yksinkertaisuuden mukana kulkevat helppo ylläpito ja bugittomuus.
Yhtenä perusteluna tuolle rakenteen muutokselle voisi pitää sitä, että palvelin loogisesti liittyy vain yhteen ohjelmaan ja asiakas vain yhteen palvelimeen, jolloin on turha tehdä tästä tiedosta muuttuvaa parametria, vaan vakiojäsen on selvästi paikallaan.
Tietenkin voin tehdä miten haluan, mutta mietin olisiko tuosta tavasta jotain erityistä hyötyä tässä tapauksessa.
Projektina on korttipeli, jossa voisi 2-4 pelaajaa pelata samassa pelissä. Olen luonut mielestäni aika hyvän pohjan siihen jo, mutta mietin miten korttipakan tietoja saataisiin järkevästi siirreltyä. Koska yhden pitää olla se palvelin niin palvelimen pitäisi luoda se pakka ja jakaa sen tietoja. Toisaalta, jos siirrän tuolla yllä olevalla socketilla sitä, niin se voisi olla vähän kömpelöä. Pitäisikö kortin noston yhteydessä kysyä palvelimelta seuraavaa korttia, jotta se voisi hallita pakkaa paremmin?
Sekin mietityttää, että miten mahtaa onnistua ohjelmakohtaisesti käyttäjän korttien hallitseminen? Jos siis palvelin pitäisi koko pakan, ja sieltä voitaisiin netin yli nostaa kortteja, niin palvelimella toimisi jokin tämännäköinen toteutus (Palvelinkin osallistuu peliin).
package korttipeli; public class Käsi { private Kortti[] kortit; Käsi(Pakka p) { kortit = new Kortti[3]; for(int i = 0; i < 3; i++) { kortit[i] = p.nostaKortti(); } } public void näytäKortit() { for(int i = 0; i < 3; i++) { System.out.println(kortit[i]); } } }
näytäKortit-metodia tosin pitäisi muuttaa siten, että se päivittää kortit myös käyttöliittymän puolelle. Mutta millainen versio tuosta pitäisi tehdä asiakkaalle, kun sehän ei tiedä tuosta pakasta mitään?
Alussa puhut vain kahdesta koneesta ja nyt sinulla on siinä jo useampi.
Vaikea nähdä tuota sinun oikeaa tavoitetta. (miksi et halua serveriä jne)
Jos yrität tehdä usean koneen chättiä, jossa ei ole serveriä, niin sellainen kannattaa unohtaa. Teoriassa mahdollista, käytännössä ei. (verkonhan voisi sitten kuka tahansa särkeä)
AINA kun sinulla on useampi kuin 2 konetta, pitää olla joku serveri (se voi toki olla yksi clienteistä) johon kaikki ottavat yhteyden. Ja tämä serveri on sitten se, johon luotetaan. (onhan sekin mahdollista, että serverin sammuessa "äänestetään uusi" call of dutyn tyyliin, mutta monimutkaisuus kasvaa jälleen rajusti)
En aivan ymmärtänyt tuota selostustasi. Suosittelen, että suunnittelet homman kunnolla pala kerrallaan riittävän pieninä palasina.
Monen pelaajan pelissä voit joko kierrättää kaikki siirtoyritykset palvelimen kautta tai antaa pelin alussa saman pakan kaikille ja välittää palvelimelle vasta onnistuneen siirron. Jälkimmäinen tapa on monessa suhteessa kätevämpi mutta teoriassa altis huijaukselle.
Tässä on eräs mahdollinen luokkalistaus mietittäväksesi:
Jos hienompi abstraktio kiinnostaa, voit tehdä Palvelimen ja Asiakkaan niin, etteivät ne itsessään sisällä verkkotoimintoja vaan yleispätevämmät send- ja receive-metodit. Sitten voit tehdä erikseen paikallisen version, TCP-version ja vaikka välityspalvelinta käyttävän HTTP-version.
Kirjoitin aluksi väärin, kun puhuin kahdesta koneesta. Tarkoitus oli sanoa kahdesta neljään tietokonetta.
Olen jo viestien lähettämiseen keksinyt ratkaisun, jonka lähetinkin aikaisemmin. Muutkin tiedot sitten kulkevat siinä mukana. Lähetän pakan siis asiakkaalle, ja vastaanotan valmiin siirron. Selvä. Ajattelin vain, että palvelimen olisi helmpompi kertoa sille mikä kortti tulee seuraavana, koska muutkin nostavat pakasta kortteja. Asiakashan ei tiedä mitään muista asiakkaista.
Pointtini oli, että et lähetä pakkaa joka välissä vaan lähetät sen vain kerran pelin alussa ja lähetät lisäksi kaikkien muiden pelaajien siirrot. Pakkohan ne siirrot on lähettää, että käyttöliittymä osaa näyttää pöydällä oikeat kortit.
Tietenkin pitää, mitäs ajattelin kun en sitä tullut ajatelleeksi. Teenkin sen noin, keksin heti tavankin miten niitä käsitellään.
Nyt näillä voi jo keskustella keskenään, tosin jos asiakas lähettää viestin, muut eivät nää sitä. Jos serveri lähettää viestin, kaikki näkevät. Jälkimmäinen toimii sen takia, että viestin lähetys toimii seuraavassa järjestyksessä: Käyttöliittymä > ServerSocketListener -> lahetaViesti(String viesti). Jotta asiakkaan viesti saataisiin lähetettyä serveriltä muillekkin, pitäisi viestiä liikuttaa näin: MultiServerThread -> ServerSocketListener -> lahetaViesti(String viesti). Jos kutsun lahetaViesti-metodia MultiServerThreadista, saan virheen non-static method lahetaViesti(java.lang.String) cannot be referenced from a static context
. Kutsu oli seuraavanlainen
ServerSocketListener.lahetaViesti(...);
Tiedän mistä se johtuu, mutta en osaa korjata sitä. Enhän voi laittaa lahetaViesti-metodille static-määritettä?
lahetaViesti-metodi on tämmöinen
... public void lahetaViesti(String viesti) { for(MultiServerThread mst : threads) { mst.laheta(viesti); } } ...
Kuten aiemmin on jo sanottu, on turhaa työtä ja sotkua koodata käyttöliittymään erikseen palvelinpeli ja asiakaspeli. Käytä aina samaa Asiakas-rajapintaa. Palvelimen tarvitsee vain vastaanottaa ja tarkistaa viestit ja välittää ne sitten kaikille muille. Toisin sanoen kun käyttöliittymä kutsuu metodia lahetaViesti, viestin ei kuulu lähteä kaikille vaan vain asiakkaalta palvelimelle (vaikka asiakas ja palvelin olisivat samalla koneella). Palvelin vastaanottaa viestin ja lähettää sen sitten edelleen.
Jep. Tutkin tuota sinun koodiasi ylhäällä, ja huomasinkin sen olevan kätevämpi kuin omani (varsinkin tämän tuoreen virheeni myötä).
Ihmettelempähän vain, että mitä olet koittanut sanoa kohdassa input = new PrintStream(s.getInputStream());
, kun PrintStreamillä ei ole konstruktoria joka ottaisi parametrikseen InputStreamin. Tarkoititko input = new Scanner(s.getInputStream())
?
Lisäsin omaan versiooni myös muutaman virheenkäsittelyn, koska ne eivät pääse kääntäjästäni läpi ilman niitä. Onhan se muutenkin hyvä käsitellä virheitä.
Koska jouduin laittamaan output ja input muuttujien määrittelyt try-catch-lohkoon, niin kääntäjä myös antaa virheen, ettei output-muuttujaa ole välttämättä määritelty. Määritänkö sen nulliksi catch-lohkossa ja kutsun this.interrupt()-metodia keskeyttämiseksi? Muokkaus. Koitin ehdottamaani tapaa, ja nyt ilmoitetaan että muuttuja on saatettu jo alustaa.
Joo, tarkoitin tietenkin Scanneria, kuten muuttujan tyypistä näkyy. (Korjasin.) Ja kuten sanoin, en edes yrittänyt kirjoittaa virheenkäsittelyä, koska esimerkin paino oli tuossa luokan sijoittelussa ja viestin välityksessä.
Oikea ratkaisu mainitsemaasi poikkeusongelmaan on heittää se poikkeus ulos konstruktorista, jolloin virhetilanteessa ei lopulta luoda koko asiakasta. Virheitä ei ole mikään pakko (eikä yleensä järkevääkään) käsitellä loppuun asti juuri siinä paikassa.
Suosittelen kuitenkin käyttöliittymän kanssa kommunikointiin tuota jälkimmäistä ehdotustani, jossa siis palvelinpuolen ei tarvitse kommunikoida käyttöliittymän kanssa ollenkaan vaan kaikki keskustelu kulkee asiakasluokan kautta. Ensimmäinen koodi oli vain pieni parannusehdotus täsmälleen niihin ongelmiin, joita esitit; jälkimmäinen luettelo oli tarkemmin ajateltu lista siitä, mitä ehkä oikeasti kannattaisi tehdä. (Toki palvelimen voi tästä huolimatta toteuttaa ensimmäisen koodin tapaan, mutta kommunikaatio käyttöliittymän kanssa pitää jättää siitä lähes kokonaan pois, ellei ClientThread-luokasta viritellä ehdottamani Asiakas-luokan optimoitua ilmentymää.)
Kiitos vastauksesta.
Virheen heittäminen ulos konstruktorista olikin tässä tapauksessa hyvä idea. Lisäsin sinne throws IOException.
Mitäköhän tarkoitit Asiakas-luokan optimoidulla ilmentymällä? Entä kun sanoit "kommunikaatio käyttöliittymän kanssa pitää jättää siitä lähes kokonaan pois", tarkoititko että jätetään kaikki JOptionPane-ilmoituksetkin pois ja heitetään näistäkin virhe eteenpäin ja käsitellään se vasta itse ohjelmassa josta Palvelin-luokka on luotu? Eihän ohjelman suoritus sillon keskeyty, kuten ymmärsin sen keskeytyvän ClientThread-luokan konstruktorissa?
Hienosäätöä on aina mahdollista tehdä jälkeenpäinkin, joten uskon toteuttavani tämän aluksi ensimmäisellä ehdottamallasi tavalla, koska se käy järkeen näinkin aloittelijalle.
Muokkaus.
Miten olisi sitten järkevintä toteuttaa Asiakas-luokka? Nykyinen toteutukseni oli tehty tyhmällä tavalla, kuljetin kaikenlaisia käyttöliittymän muuttujia metodien parametreinä, jotta Asiakas-luokka voisi päivittää käyttöliittymään vastaanottamansa viestin.
Siis suosittelen lähtökohtaisesti tällaista tapaa:
void aloitaPeliPalvelimena() { try { palvelin = new TCPPalvelin(); } catch (IOException e) { JOptionPane.plaa("Palvelimen käynnistys epäonnistui: " + e.getMessage()); return; } palvelin.start(); asiakas = new TCPAsiakas("localhost", palvelin.portti); } void aloitaPeliAsiakkaana() { asiakas = new TCPAsiakas(muuOsoite, muuPortti); }
Näin käyttöliittymä voi pelin osalta toimia samalla tavalla riippumatta siitä, onko kone asiakas vai palvelin.
Tarkoitan Asiakas-luokan optimoidulla versiolla sellaista luokkaa, joka ei otakaan palvelimeen TCP-yhteyttä vaan keskustelee suoraan paikallisen Palvelin-olion kanssa. Edelliseen verrattuna käytännön hyöty on mitätön ja vaiva suunnaton, mutta jossain toisessa ohjelmassa tällaisesta voisi olla aitoa hyötyäkin. Edellinen koodi muuttuisi siis sen verran, että palvelimen käynnistyksen jälkeen asiakas luotaisiin eri tavalla:
//asiakas = new TCPAsiakas("localhost", server.portti); asiakas = palvelin.new PaikallinenAsiakas();
Macro kirjoitti:
Entä kun sanoit "kommunikaatio käyttöliittymän kanssa pitää jättää siitä lähes kokonaan pois", tarkoititko että jätetään kaikki JOptionPane-ilmoituksetkin pois ja heitetään näistäkin virhe eteenpäin ja käsitellään se vasta itse ohjelmassa josta Palvelin-luokka on luotu? Eihän ohjelman suoritus sillon keskeyty, kuten ymmärsin sen keskeytyvän ClientThread-luokan konstruktorissa?
Nyt kannattaisi pikaisesti perehtyä poikkeusten perusasioihin. Kun jostain lentää poikkeus, koodin suoritus nimenomaan hyppää vastaavaan catch-lohkoon eikä enää palaa heittopaikalle. Yleensä juuri niin kuuluu tapahtuakin: Jos saat esimerkiksi ServerSocketin accept-metodilta poikkeuksen, tuskinpa se tilanne on seuraavalla yrityksellä yhtään parempi, joten catch-lohkon kannattaisi olla silmukan ulkopuolella ja palvelimen voi saman tien sulkea. Vastaavasti jos new ServerSocket
heittää poikkeuksen, on turha edes yrittää kuunnella asiakkaita, koska palvelinta ei ole olemassakaan. (Siksi new ServerSocket kannattaisi tehdä jo ServerSocketListener-luokkasi konstruktorissa – saataisiin ongelma heti selville ilman uutta säiettä.)
Ja kyllä, kaikki JOptionPane-ilmoituksetkin pitää ottaa pois ja laittaa sinne käyttöliittymän puolelle. Palvelinkoodin pitäisi olla sellainen, että sitä voi käyttää yksinkertaisesti komentoriviohjelmassakin:
public class Komentorivikorttipelipalvelin { public static void main(String[] args) { try { Palvelin p = new Palvelin(); p.run(); } catch (Exception e) { System.out.println("Virhe, palvelin suljetaan!"); System.err.println(e.getMessage()); e.printStackTrace(); } } }
Ainoat kohdat, joissa joudut virittelemään jotain, ovat ne, joissa poikkeus tapahtuu eri säikeessä. Esimerkiksi palvelimen kuuntelusilmukan päättymisestä et saa suoraa tietoa käyttöliittymälle, joten joudut joko jättämään sen huomiotta (ihan hyvä ratkaisu harrastelijalle, harvoin siinä kesken kaiken virheitä tulee) tai välittämään poikkeuksen jotenkin muuten, esimerkiksi vastaavaan tyyliin handleException-metodilla, kuin aiemmassa koodissani viestit käsiteltiin.
Macro kirjoitti:
Miten olisi sitten järkevintä toteuttaa Asiakas-luokka?
Sen selitin jo viestissäni, jossa luettelin tarvittavat luokat. Asiakas vain ilmoittaa käyttöliittymälle (fiksulla rajapinnalla), että nyt olisi jotain tapahtunut. Käyttöliittymä sitten kysyy uudet tiedot asiakkaalta (tai Peli-oliolta).
Kiitos taas vastauksesta.
En ymmärtänyt aikaisemmin mitä tarkoitit kun puhuit käyttöliittymän toimimisesta samalla tavalla serverin ja asiakkaan tapauksissa, mutta ensimmäinen koodilistauksesi valaisi. Siinä on muuten virhe, server.portti -> palvelin.portti, eikö?
Niin, try-lohkon suoritushan siinä katkeaa eikä koko luokan. En nähtävästi ajatellut mitä kirjoitin, olen kyllä opiskellut poikkeusten käsittelyn kauan sitten.
Kysyin Asiakas-luokan toteuttamisesta, koska ymmärsin ensimmäisen ja toisen koodilistauksen välisen asian aikaisemmin väärin. Siinä tuskin on mitään kummempia ongelmia.
Macro kirjoitti:
server.portti -> palvelin.portti, eikö?
Joo, kylläpä näitä nyt tulee.
Macro kirjoitti:
Niin, try-lohkon suoritushan siinä katkeaa eikä koko luokan.
Mitähän ihmettä tarkoitat "koko luokan" suorituksen katkeamisella? Jos tarkoitit funktiota etkä luokkaa, niin kyllähän siinä funktionkin suoritus katkeaa, jos se catch-lohko ei ole kyseisen funktion sisällä.
Mutta hyvä, että kaikki on nyt selvää. :)
Joo, niinhän siinä käy.
Otin mallia palvelinkoodistasi, ja tein asiakkaasta vastaavan näköisen. Tosin tämä ei sisällä niitä rajapintoja, jotka palvelin sisältää.
package korttipeli; import java.io.IOException; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; import java.util.Scanner; import javax.swing.JOptionPane; public class Asiakas extends Thread { private final int portti; private final String osoite; private final Socket socket; private final PrintWriter out; private final Scanner in; public Asiakas(String osoite, int portti) throws UnknownHostException, IOException { this.osoite = osoite; this.portti = portti; socket = new Socket(osoite, portti); out = new PrintWriter(socket.getOutputStream()); in = new Scanner(socket.getInputStream()); } @Override public void run() { while(!isInterrupted() && in.hasNextLine()) { System.out.println(in.nextLine()); } } @Override public void interrupt() { try { socket.close(); } catch(IOException ioe) { JOptionPane.showMessageDialog(null, ioe, "Virhe!", JOptionPane.ERROR_MESSAGE); } super.interrupt(); } public void lähetäViesti(String viesti) { out.println(viesti); out.flush(); } }
Onko tämä millään mittapuulla käypä ratkaisu tarkoitukseeni?
Kysyisin siitä vielä pari kysymystä.
1. Kannattaako muuttujien määritteleminen final-avainsanalla, kun niitähän ei tarvitse uudelleen alustaa?
2. Pitäisikö lähetäViesti-funktio synkronoida synchronized-avainsanalla?
3. Miten run-funktion kannattaisi palauttaa käyttöliittymälle saamansa data? Sanoit aikaisemmin
Metabolix kirjoitti:
käyttää viestien välitykseen täysin erillistä luokkaa (kuten jonoa, johon työnnät viestejä ja jota luet pääohjelman silmukassa)
Mielestäni ihan hyvä idea, mutta silloin minun pitäisi luoda uusi säie ja silloinhan tämä irtautuisi käyttöliittymästä. Vai teenkö luokan pääluokan sisälle? Mutta kun se ei voi olla silloin määritetty publiciksi, niin Asiakas-luokka ei näe sitä.
Taas on JOptionPane väärässä paikassa. Sitä paitsi tuolle virheelle et kuitenkaan voi tehdä mitään, joten turha siitä on käyttäjälle edes ilmoittaa. Tulosta vaikka System.err-virtaan, jos haluat.
Et tarvitse palvelinkoodille aiemmin luettelemiani rajapintoja, koska kaikki kommunikaatio käyttöliittymästä tapahtuu Asiakas-luokan kautta.
Oikeat avainsanat (myös final) ehkäisevät virheitä, ja final voisi teoriassa auttaa kääntäjää myös koodin optimoinnissa.
Jos olet epävarma siitä, voiko asiakas jotenkin lähettää monta viestiä samaan aikaan, synkronoi. Menetetty aika on korttipelissä merkityksetön.
Kuten ehdotin jo monta kertaa, laita Asiakas-luokka pyörittämään korttipelin logiikkaa (Peli-oliota) ja kutsu lopuksi käyttöliittymän metodia pelinTilaMuuttunut. Loppu hoida käyttöliittymän puolella. Taas kerran koodista tulee siis sellainen, että voit koodata koko käyttöliittymän uudestaan koskematta enää ollenkaan Asiakas-luokkaan.
Se onkin väärässä paikassa, se ja muut virheilmoitukset ohjataan valmiissa ohjelmassa esimerkiksi sinne System.err-virtaan eikä niitä käyttäjälle näytetä (ainakaan suoraan Javan palauttamia virheitä).
Ihmettelinkin mikä hyöty noissa rajapinnoissa on, vaikka en ole pahemmin niihin tutustunut (lainasin Java-kirjan viime vuodelta tutustuakseni). Mielestäni kysyinkin siitä aikaisemmin, taisinkin vain miettiä sitä.
Käytännössä siis final-avainsanasta ei ole mitään erikoisempaa hyötyä tapauksessani.
Synkronoin viestinlähetysfunktion vaikka en usko käyttäjän mitenkään pystyvän lähettämään viestejä samaan aikaan. Kuten sanoitkin, synkronoinnin takia menetetty aika ei ratkaise mitään.
Koska ei ole järkeä tehdä asioita päinvastaisessa järjestyksessä, ja palkata miehistöä ennen laivan rakennusta, niin olen toteuttanut verkkotoiminnot ennen pelin logiikkaa. Näin voin heti alkaa rakentamaan sitä heti toimivaksi verkkotoimintoineen.
En ole reagoinut aikaisempiin ehdotukseesi vielä pelin logiikasta, koska kuten ylempänä sanoin, olen rakentanut verkkotoiminnot aluksi jotta voin liikutella pelin dataa. Ideasi oli kyllä hyvä, olisin itse varmaan laittanut pelin toiminnallisuuden käyttöliittymän puolelle. Hyvä että mainitsit asiasta.
Tuo pelinTilaMuuttunut-funktion toteutus oli myös hyvä idea, koska sitä kutsuttaessa käyttöliittymä voisi itse hakea tiedot joko Asiakas- tai Peli-luokalta ilman että sen pitäisi vastaanottaa Asiakas- tai Peli-luokalta ne.
Voiko muuten JScrollPaneen liittää jonkun taulukkotyylisen elementin, johon saisin liitettyä kortteja? Onhan olemassa JTable, mutta onko sellainen vähän kankea tähän? Nettisivupuolella olen oppinut välttämään taulukoita ulkoasun tekemisessä, joten se on nytkin vähän niin ja näin. Koska korttien määrä ei pysy aina samana, niin koodin puolelta pitäisi pystyä lisäämään omatKortit-elementtiin (tällä hetkellä JPanel) kortteja. Ajattelin säilöä kortit ArrayListissä ja piirtää ne siitä.
Nykyinen toteutus on tehty korttien osalta Netbeansin design-puolelta heittämällä JPaneliin JLabeleita, joille olen laittanut ikoniksi korttikuvan.
Muokkaus.
Ajattelin korttien piirtämisongelmaa uudelleen vähän joka sortilta, ja mielestäni olisi ehkä paras korvata JPanel JScrollPanelilla, jolla on vakiokorkeus ja leveyden ylittyessä se saisi vierityspalkin. Näin kortteja olisi helppo selata, ja niiden lisääminenkin voisi olla ihan helppoa. Googlella löysin myös miten JLabeleille saadaan paddingia, joten en uskokkaan että käsikorttien kanssa on ongelmaa.
Macro kirjoitti:
Käytännössä siis final-avainsanasta ei ole mitään erikoisempaa hyötyä tapauksessani.
Jos tuolle linjalle lähdetään, niin saman tien voidaankin tehdä kaikista jäsenistä julkisia, unohtaa käyttöliittymän erotus logiikasta ja muokata joka paikasta suoraan staattista käyttöliittymäoliota, koska eihän muistakaan hyvistä ohjelmointitavoista ole "mitään erikoisempaa hyötyä tapauksessasi".
Monien hyvien ohjelmointitapojen ei ole tarkoituskaan olla välittömästi hyödyllisiä. Mutta usko pois, kyllä niistä vain on suunnatonta iloa pidemmän päälle.
Macro kirjoitti:
Voiko muuten JScrollPaneen liittää jonkun taulukkotyylisen elementin
Voit tehdä paneelin, jolla on GridLayout. Silloin sisältö menee tasakokoisiin ruutuihin.
Toinen mahdollisuus koko käyttöliittymään on piirtää kortit itse Canvas-elementille tai asetella korttielementit käsin ilman LayoutManageria. Näillä vaihtoehdoilla saat paljon joustavamman tuloksen.
Itse ehkä tekisin niin, että peli sisältäisi erilaisia pakkatyyppejä (ei kortteja esillä, yksi kortti esillä, kaikki kortit esillä; päällimmäisen nosto, minkä tahansa nosto). Käyttöliittymä sitten voisi vain hakea kaikki pakat ja asetella ja piirtää ne jollain logiikalla. Ihan kaikkiin peleihin tällainen järjestely ei varmasti taivu, mutta isossa osassa korttipeleistä on käsikorttien lisäksi vain nostopakka, poistopakka ja kierroksen lyödyt kortit.
Hyvistä ohjelmointitavoista on paljonkin hyötyä, mutta sen käsityksen sain viestistäsi, ettei final-avainsanan puuttuminen muuta koodin toimintaa mitenkään ja niinhän se on. En ottanut sitä pois koodista, koska eihän muuttujaa ole tarve muuttaa missään vaiheessa.
En ole ohjelmoinut vielä niin kauan ja suuria projekteja, että olisin kokenut sen suunnattoman ilon. Tekemällä oppii.
Hyvä idea. Koska käytössä on IDE kehitystä varten, oli helppo testata että mikä layoutti sopi siihen parhaiten. Päädyin kumminkin FlowLayouttiin, koska tämä teki mielestäni paremman lopputuloksen kuin GridLayout. Pienellä korttimäärällä GridLayout levitti ne koko elementin alueelle, koska antoi niille yhtä suuren tilan. FlowLayoutilla sain laitettua elementeille itse paddingin määrän.
Kiitos jälleen kerran vastauksestasi. Taas kerran siitä oli hyötyä, vaikkei toteutus mennytkään ihan niin kuin sanoit.
Ohjelmoinnissahan on paljonkin juttuja jotka ei vaikuta lopputulokseen millään tavalla. Esimerkiksi do-loopin voi korvata for-loopilla ja päin vastoin eikä koneen suorittaman konekielen välillä välttämättä ole mitään eroa, mutta silti kannattaa käyttää sitä joka semanttisesti vastaa paremmin sitä mitä ollaan tekemässä.
Ohjelmoinnissa ei kannata käyttää mitään ominaisuutta, joka ei tunnu omasta mielestä hyödylliseltä. Esimerkiksi jos final-sana tuntuu turhalta, sitä ei kannata käyttää. Hyvät ohjelmointitavat ovat mielipiteitä, eikä kannata uskoa sokeasti "viisaamman" sanaa. Kannattaa kuitenkin aina miettiä ja ottaa selvää, minkä takia kielen eri ominaisuudet ovat olemassa.
Edellisestä olen jokseenkin samaa mieltä jos koodailee yksikseen projekteja joita kenenkään ei ole tarkoitus ylläpitää tai jatkokehittää.
Olen jokseenkin eri mieltä. Omankin koodin katsominen pitkän ajan kuluttua on vaikeaa, jos se on huonosti kirjoitettu. Sitä paitsi jos ei yksin koodaillessaan noudata ollenkaan hyviä tapoja, niitä voi olla työlästä omaksua sitten aikanaan, kun vaikka töissä vaaditaan. Ja kääntäen: kun oppii käyttämään vaikkapa final-sanaa, sen kirjoittaminen ei ole enää turhaa ja vaivalloista vaan se on luonnollinen ja asianmukainen osa koodia ja lähes yhtä lailla hyödyllinen kuin muuttujan järkevä nimi.
Jos koodia ei ole tarkoitus ylläpitää itsekään, voi toki kirjoittaa, miten vain nopeiten kykenee.
Tietysti paras vaihtoehto on koodata paljon molemmilla tavoilla ja todeta sitten kokemuksen kautta, kumpi tapa on mukavampi ja tehokkaampi ja miten valinta riippuu tilanteesta. Itse olen tällä menetelmällä päätynyt useimmissa tapauksissa hyviin tapoihin – vaikka noita huonompia koodeja vielä kummitteleekin esimerkiksi putkapostiratkaisuissa. Huonosti kirjoitetun koodin maksimipituutena pidän yhtä kohtuullisen kokoista tiedostoa. :)
Grez kirjoitti:
Edellisestä olen jokseenkin samaa mieltä jos koodailee yksikseen projekteja joita kenenkään ei ole tarkoitus ylläpitää tai jatkokehittää.
Tämä lienee tilanne monella meistä.
Metabolix kirjoitti:
Olen jokseenkin eri mieltä. Omankin koodin katsominen pitkän ajan kuluttua on vaikeaa, jos se on huonosti kirjoitettu.
Miten sellainen hyvä käytäntö auttaa, jonka hyötyä ei edes ymmärrä itse?
Metabolix kirjoitti:
Tietysti paras vaihtoehto on koodata paljon molemmilla tavoilla ja todeta sitten kokemuksen kautta, kumpi tapa on mukavampi ja tehokkaampi ja miten valinta riippuu tilanteesta.
Tämä on totta. Moni asia tuntuu turhalta, jos ei tutustu siihen kunnolla.
Antti Laaksonen kirjoitti:
Miten sellainen hyvä käytäntö auttaa, jonka hyötyä ei edes ymmärrä itse?
Tuolta kannalta olet aivan oikeassa. En missään vaiheessa tarkoittanutkaan, että pitäisi tehdä mekaanisesti tietyllä tavalla ymmärtämättä, mistä on kyse. Minusta pääasia on aina ymmärtää, mitä merkinnät tarkoittavat. (Jos jotain ei vielä ymmärrä, parempi ottaa selvää kuin vain unohtaa koko asia.) Yleensähän merkinnän tarkoitus on kertoa sekä koodarille että kääntäjälle jostain asiasta, jonka muuten joutuisi kirjoittamaan kommenttiin.
Ei ole harvinaista nähdä tällaisia ratkaisuja:
public class X { public static int y = 1; // Ei saa muuttaa! }
Usein selittävä kommenttikin puuttuu, koska "kyllähän sen muistaa" tai "ei tätä koodia varmaan enää muokata". Asiahan olisi yhdellä final-sanalla selvä, eikä erehdyksiä voisi vahingossakaan sattua.
Ihan hyviä mielipiteitä. Metabolix neuvoi käyttämään final-avainsanaa, selvitin mitä se tekee ja päätin käyttää sitä. Koska nyt tiedän sen tehtävän, olen osannut pistää sen muutamaan muuhunkin paikkaan eikä se tuota vaivaa kirjoittaa sitä.
Oman koodin tutkistelu voi olla vaikeaa hyvinkin lyhyen ajan päästä kirjoitushetkestä, jos se on tehnyt huonosti. Muuttujien nimeäminen järkevästi on minusta se yksi tärkeimmistä selkeyttävistä asioista sisennyksen lisäksi. Jos muuttuja on huonosti nimetty, ei seuraavana päivänä muista mihin se liittyy jollei tutki missä se alustetaan. Sama juttu on metodien nimeämisessä. Ehkä pidemmän nimen kirjoittaminen vie puolikkaan sekunin kauemmin, mutta se kannattaa, koska myöhemmin sen tehtävän selvittämiseen menee kauemmin kuin se puoli sekuntia. Esimerkiksi tässä keskustelussa annettu pelinTilaMuuttunut-nimestä tajuaa heti mitä se tarkoittaa, toisin kuin jos olisi kirjoitettu funktio().
Koska pelinTilaMuuttunut-metodin pitää olla staattinen jotta sitä voidaan kutsua ilman uuden ilmentymän luomista käyttöliittymästä, se estää joitakin toimintoja toimimasta. Esimerkisi Random-luokka jää pois, joka olisi tarpeellinen.
Onko jotain tapaa kutsua Asiakas-luokasta käyttöliittymäluokan metodia ilman, että sen täytyisi olla staattinen? Asiakas-luokan ilmentymä on luotu käyttöliittymästä.
Macro kirjoitti:
Koska pelinTilaMuuttunut-metodin pitää olla staattinen jotta sitä voidaan kutsua ilman uuden ilmentymän luomista käyttöliittymästä, se estää joitakin toimintoja toimimasta. Esimerkisi Random-luokka jää pois, joka olisi tarpeellinen.
En näe miten nämä asiat riippuu toisistaan.
Macro kirjoitti:
Onko jotain tapaa kutsua Asiakas-luokasta käyttöliittymäluokan metodia ilman, että sen täytyisi olla staattinen? Asiakas-luokan ilmentymä on luotu käyttöliittymästä.
Totta kai. Voit aina antaa Asiakas-luokalle viittauksen käyttöliittymäluokka-olioon esimerkiksi siinä vaiheessa kun luot Asiakas-luokan ilmentymän.
Ja sitten jos sinulla on ohjelmassa vain yksi ilmentymä käyttöliittymäluokasta, niin voit laittaa sinne jopa staattisen muuttujan joka osoittaa siihen ainoaan ilmentymään. Se kannattaa sitten muistaa poistaa käyttöliittymää sulkiessa ettei se jää roikkumaan.
Toisesta luokasta ei voi kutsua toisen luokan metodia ellei se ole staattinen tai siitä ole luotu uutta ilmentymää. Staattisessa metodissa ei voi käyttää ei-staattisia eli dynaamisia (?) metodeja.
Kiitos, empä tullut ajatelleeksi että sitä voi kuljettaa parametrinä.
Mikä idea olisi laittaa staattinen muuttuja osoittamaan itseensä?
Macro kirjoitti:
Mikä idea olisi laittaa staattinen muuttuja osoittamaan itseensä?
No siis sillä voisi tehdä alla kuvatun kaltaisen virityksen. En nyt sano että siinä olisi järkeä, mutta kun kerran kysyit.
En nyt jaksanut säätää Javaa, toivottavasti C#:n syntaksi ei eroa liikaa
public class Koe { private static Koe LastInstance; public Koe() { LastInstance = this; } public void EiStaattinenMetodi() { //Tätä ei voi kutsua ilman instanssia } public static void StaattinenMetodi() { //Tätä voi kutsua ilman instanssia LastInstance.EiStaattinenMetodi(); } }
Macro kirjoitti:
Koska pelinTilaMuuttunut-metodin pitää olla staattinen jotta sitä voidaan kutsua ilman uuden ilmentymän luomista käyttöliittymästä
Staattisuus on Javassa kohtalaisen harvoin oikea ratkaisu ongelmiin. Esimerkiksi juuri tuossa se on aika ruma temppu, koska silloin tulee taas kovakoodattua väärään paikkaan suora viittaus käyttöliittymäkoodiin. Tässä voit nyt soveltaa tuota aiemman koodini MessageHandler-tapaa: tee rajapinta PelinTilaMuuttunutHandler (tai Listener, kuten Javan omat luokat yleensä ovat), toteuta rajapinta käyttöliittymässä ja välitä tätä tyyppiä oleva parametri Asiakkaalle.
(Tietenkin jos ei ole tarkoituskaan käyttää olion sisältöä, staattisuus on samalla tavalla oikea ratkaisu kuin taannoisessa keskustelussa final-sanan käyttö.)
Niin ja tarkennan vielä että itsekin suosittelen ehdottomasti Metabolixin ehdottamaa lähestymistapaa. Tulee jo riippuvuusgraafeiltaan paljon järkevämpiä luokkajoukkoja jos riippuvuudet on lähtökohtaisesti yksisuuntaisia.
Eli siis jos käyttöliittymäoliosi osaa käyttää ja käyttää oliota X, niin sen olion X ei tarvitsisi tietää mitään käyttöliittymäoliosta tai sitten jos tarvitsee tietää jotain niin käyttöliittymäolio kertoo X:n tarjoamilla keinoilla. Näin voit käyttää komponenttia X myöhemmin jossain muussa yhteydessä. Jos teet oliot täysin toisistaan riippuvaisiksi, niin sittenhän olisi melkein sama tehdä vain yksi olio.
En kyllä mielestäni ihan noinkaan kysynyt, mutta ymmärsit aluksi kumminkin mitä tarkoitin.
Kiitos taas Metabolix, toteutin tuollaisen handlerin sinne, ja hyvin pelittää. Kysyn kumminkin, että miten tämä eroaa siitä, että kuljettaisi Grezin ehdottamalla tavalla käyttöliittymästä viittausta Asiakas-luokalle? Lopputuloksessa se ei eroa mitenkään, mutta tuleeko jotain hyötyjä tai haittoja (vaikka yksinkertaisuuden mukana kulkevat bugittomuus ja helppo ylläpidettävyys)?
No kuten jo kirjoitin tuossa äsken, niin ainakin uudelleenkäytettävyys on ihan eri luokkaa jos se apuluokkasi on riippumaton käyttöliittymäluokasta.
Nythän jos teet sen käyttämään tuon nykyisen projektisi käyttöliittymäluokan metodeita ja muuttujia, niin et voi käyttää sitä suoraan seuraavassa projektissasi tai samassa projektissa muussa yhteydessä.
Totta. En usko, että tulen käyttämään sitä uudelleen ainakaan tässä työssä, mutta voihan näin opetella niitä ns. hyviä ohjelmointitapoja.
Kiitos avusta.
Muutaman päivän olen selvinnyt ilman apuja, mutta viimeiset kaksi ovat menneet tuskaillessa NullPointerExceptionin kanssa.
Käyttöliittymäluokan pelinTilaMuuttunut-kutsuu Asiakas-luokan kautta Peli-luokan annaKäsikortit-metodia, joka palauttaa ArrayListin tyypiltään Kortti. Olen luonut ArrayListin käyttöliittymän puolella, joka on samaa tyyppiä. Kun koitan asettaa ArrayList<Kortti> annetutKäsikortit = _asiakas.peli.annaKäsikortit(), saan NullPointerExceptionin. Se ei johdu _asiakas-muuttujasta, eikä silloin voi myöskään johtua asiakasluokan peli-muuttujasta. Virhe ei voi olla siinä, koska sen kautta onnistuu viestin lähettäminenkin.
annaKäsikortit-metodi näyttää tälläiseltä
public ArrayList<Kortti> käsikortit = new ArrayList<Kortti>(); public ArrayList<Kortti> annaKäsikortit() { return käsikortit; }
Muokkaus. Virhe tulikin liian aikaisesta pelinTilaMuuttunut-metodin kutsusta. ArrayListia ei ollut ehditty vielä alustaa, ja sen arvoksi tuli null.
Olen mielestäni ihan hyvässä vaiheessa, mutta ongelmani on MouseListenerin kanssa. Olen koittanut lisätä MouseListeneriä omille käsikorteille. Se onnistuukin, mutta en tiedä mitä näistä on painettu. Olen piirtänyt kortit näin
for(int i = 0; i < annetutKäsikortit.size(); i++) { URL _kuva = this.getClass().getResource("/kortit/" + annetutKäsikortit.get(i).haeNumero() + "_pieni.png"); JLabel label = new JLabel(new ImageIcon(_kuva)); label.addMouseListener(hiirenkuuntelija); omatKortit.add(label); }
omatKortit on JPanel-tyyppinen elementti, johon ne sitten piirretään. hiirenkuuntelija on luokka, joka toteuttaa MouseListener-rajapinnan. En tiedä miten selvittäisin mikä Kortti-olio sieltä klikkauksen takaa löytyy.
Macro kirjoitti:
En tiedä miten selvittäisin mikä Kortti-olio sieltä klikkauksen takaa löytyy.
MouseEvent (http://download.oracle.com/javase/6/docs/api/
getComponent()
Tuolla saa selville mikä Component olio aiheutti kyseisen MouseEventin.
getComponent palauttaa jonkin epämääräisen merkkijonon.
lainaus:
javax.swing.JLabel[,219,5,70x88,alignmentX=0.0,
alignmentY=0.0,border=javax.swing.plaf.synth. SynthBorder@25d2b2,flags=8388608,maximumSize=, minimumSize=,preferredSize=,defaultIcon=file:/home/.../ NetBeansProjects/.../build/classes/kortit/ 33_pieni.png,disabledIcon=,horizontalAlignment=CENTER,horizontalTextPosition=TRAILING,iconTextGap=4,labelFor=,text=,verticalAlignment=CENTER,verticalTextPosition=CENTER]
Näkeehän tuosta mikä kuva siellä on (tässä 33_pieni.png), mutta en voi päätellä sen perusteella mikä Kortti-olio sieltä löytyisi.
Macro kirjoitti:
omatKortit on JPanel-tyyppinen elementti, johon ne sitten piirretään. hiirenkuuntelija on luokka, joka toteuttaa MouseListener-rajapinnan. En tiedä miten selvittäisin mikä Kortti-olio sieltä klikkauksen takaa löytyy
En Javaa osaa pätkääkään, mutta kokeillaan...
Olet asettanut MouseListenerit JPaneliin piirtämillesi JLabeleille, jotka kuvastavat kortteja, onko näin? Olen aika varma, että joudut kyllä varmasti itse pitämään huolta, että tiedät mikä kortti on sijoitettu mihinkin JLabeliin.
Kyllä, juuri noin olen ne asettanut. Mitenkäs pidän niistä huolta, kun JLabeleita ei voida luoda ennen kuin niitä tarvitaan? Jos luon JLabel-tyyppisen ArrayListin, säilön sinne JLabelin mutta en silti tiedä mitään Kortti-oliosta vaan JLabelista (ja ehkä mahdollisesti mikä listan indekseistä aiheutti kutsun).
Macro kirjoitti:
Kyllä, juuri noin olen ne asettanut. Mitenkäs pidän niistä huolta, kun JLabeleita ei voida luoda ennen kuin niitä tarvitaan? Jos luon JLabel-tyyppisen ArrayListin, säilön sinne JLabelin mutta en silti tiedä mitään Kortti-oliosta vaan JLabelista (ja ehkä mahdollisesti mikä listan indekseistä aiheutti kutsun).
Jos siis MouseListener metodissasi on niin että MouseEvent:n aiheuttaja (joka siis selviää getComponent() kutsulla) on JLabel voit tehdä esim. näin:
Labelien luontivaiheessa pistät talteen tyyliin:
Map<JLabel, KorttiOliosi> labelCardMap = new HashMap<JLabel, KorttiOliosi>();
JLabel label = ... miten se nyt luodaankaan ... new JLabel(KorttiOlio ko);
labelCardMap.put(label, ko);
nyt mouselistenerissä voit tehdä:
KorttiOlio ko = labelCardMap.get(mouseEvent.getComponent());
Joo, tutkinkin asiaa, ja keksinkin eräästä kirjasta tuon Map-tietorakenteen. Oli ihan päässyt unohtumaan, että sellainenkin on.
Muokkaus. Ai katos, toi getComponen-metodi palauttikin oikeasti sen lähteen. Tutkin aikaisemmin vain sitä tulostusta, en ajatellut sen palauttavan komponenttia oikeassa muodossaan.
Mitenköhän olisi järkevintä ja helpointa vertailla, mikä kortti menee minkäkin päälle? Tässä (eikä kyllä missään tuntemassani korttipelissä) korttien maat eivät ratkaise minkä päälle ne sopivat, joten toteutus on yksinkertaisempi kuin jos maatkin pitäisi huomioida. Ajattelin numeroida kortit seuraavasti
Kortti | Numero |
Ässä | 1 |
2-10 | 2-10 |
Jätkä | 11 |
Kuningatar | 12 |
Kuningas | 13 |
Silloinhan voisin verrata, että jos kortti jota yritetään laittaa pakkaan on suurempi tai yhtä suuri arvoltaan kuin pakan päällimmäinen, se sopii sinne. Muutama poikkeus kyllä on, esimerkiksi kymppi ja kakkonen sopivat jokaisen kortin päälle.
Nähtävästi keksinkin tuon korttien tarkistelemisen tässä kun kirjoittelin tätä viestiä. Voikohan sitä kumminkin parantaa?
Toinen ongelma on vuorojen kanssa. Miten tarkistetaan kenen vuoro on? Todennäköisesti serverin tulisi pitää siitä tietoa, eikö?
Kaikkea tuota varten piti nyt olla se Peli-luokasta periytyvä Ristiseiska-luokka (tai mitä ikinä pelaatkin), joka tietää, mitä kortteja juuri tässä pelissä voi lyödä ja kenen vuoro sitten on. Kuten kauan sitten ehdotin, olisi kätevää, jos asiakkaat alustaisivat pelin alussa tilanteensa ja pyörittäisivät sitten peliä itse. Palvelimen ei siis tarvitse erikseen pitää kirjaa mistään, vaan palvelin vain välittää toteutuneet siirrot asiakkaalta toiselle.
Pääsit vihdoin koodauksesta varsinaiseen ohjelmointiin, jossa koodia tulee vain pari riviä mutta niitä rivejä pitää miettiä pidempään. Suosittelen, että mietit pelilogiikan ihan itse, koska se on monessa mielessä tärkein osa koko projektistasi.
Älä mielellään merkitse kortteja pelkästään numeroilla vaan tee enum-tyypit maista ja arvoista. Voit toki sisällyttää arvoihin numerot helpottamaan vertailuja, mutta eräät poikkeukset joudut kuitenkin koodaamaan erikseen.
Useimmissa peleissä, jotka minulle tulevat mieleen, kortin maallakin on paljon merkitystä. Esimerkiksi ristiseiskassa kuhunkin pakkaan sopii tasan yksi kortti ja hertassa pitää lyödä ensisijaisesti samaa maata tai valttia.
Tuo Ristiseiska-nimi vähän hämäsi, en heti keksinyt mikä sen tarkoitus on. Luokkarakenne on vähän erillainen kumminkin kuin aikaisemmin esittämäsi, Peli-luokka ei ole abstrakti ja se sisältää itsessään jo pelin logiikan. Nyt pidemmän päälle ymmärrän miksi olisi ollut hyötyä tehdä toisella tavalla, kun alkaa tulla juuri sitä mainitsemaasi varsinaista ohjelmointia.
Totta, se onkin tärkein osa, mutta ei sillä silti pärjäisi ilman muita osia eikä muilla ilman sitä.
En ole ihan perillä enum-tyypeistä, vaikka kirjasta niistä olen lukenut. Voisin tutustua.
En tiedä sitten tästä pelistä, että miten se on tiedossa muilla. Google ei ainakaan kertonut mitään tästä. Ehkä se on vähän sekoitettu versio muista peleistä, joka on kulkenut ilman virallisia sääntöjä.
Muokkaus. Ihan totta muuten, koodia tuli oikeastaan yksi rivi (83 merkkiä) ja se riitti tarkistamaan sopiiko kortti pakkaan.
83 merkkiä ei kumminkaan ihan riittänyt tarkistamaan siirron laillisuutta, mutta ei se paljon pidemmäksi tullut.
Olen nyt nelisen päivää ollut jumissa, kun pitäisi tarkistaa vuoroja. Tässä vaiheessa ohjelma toimii siten, että asiakas antaa liittyessään tiedon pienimmästä kortistaan serverille ja serveri pistää sen TreeSet-mappiin (käsittääkseni tämä osaa lajitella tiedot tämän arvojen mukaan). Nyt kun serveri saa viestin X, se lähettää taulukon kaikille asiakkaille. Hyvä juttu! Mutta mitenkäs asiakkaan pitäisi tunnistaa itsensä taulukosta, kun monella käyttäjällä voi pienimpänä korttina olla sama kortti? Nyt avaimena on Metabolixin aikaisemmin esittämä ClientThread-olio.
Mitä jos yksinkertaisesti jakaisit pelaajille numerot liittymisjärjestyksessä? Tietenkään ei pidä käyttää mitään ClientThreadeja tunnistamiseen, hyvänen aika. Sitä paitsi palvelimen ei tarvitse puuttua tähänkään asiaan: kun asiakkaat tietävät numeronsa pelin alussa ja saavat palvelimelta saman pakan, ne osaavat jakaa kortit oikein ja tietävät paikallisesti, kuka aloittaa.
Jos nyt ajatellaan tietoturvaa, niin en lähettäisi pakkaa asiakasohjelmille ollenkaan, koska se suo jollekin häxörille liian suuren etulyöntiaseman.
Totta, Metabolix. Numeroiden jakaminen on ihan hyvä idea, en tullut ajatelleeki. Sen kyllä mietin jo valmiiksi, että asiakas pitää huolta omasta vuorostaan.
The Alchemist, en ole pahemmin perehtynyt hakkerointiin, mutta vaikka tuo toisikin etulyöntiaseman, ei ole kumminkaan kovin suuri murhe jos hakkeri haluaa kuluttaa aikaansa pelin voittamiseen. Tietenkään se ei ole kiva muille, mutta ei siitä suurempaa haittaakaan ole kenellekkään.
Macro kirjoitti:
The Alchemist, en ole pahemmin perehtynyt hakkerointiin, mutta vaikka tuo toisikin etulyöntiaseman, ei ole kumminkaan kovin suuri murhe jos hakkeri haluaa kuluttaa aikaansa pelin voittamiseen. Tietenkään se ei ole kiva muille, mutta ei siitä suurempaa haittaakaan ole kenellekkään.
Mutta tässä on taas jälleen kyse siitä, miten asia tehdään oikein. Nyt ehkä jo tiedät, miksi pitää olla palvelin. Ja oikea tapa on, että clienteillä on mahdollisimman vähän tietoa, eivätkä clientit "tee päätöksiä" vaa lähettävät vain palvelimelle pyynnöt / siirrot jotka palvelin hyväksyy / hylkää.
Meitzi kirjoitti:
– – eivätkä clientit "tee päätöksiä" vaa lähettävät vain palvelimelle pyynnöt / siirrot jotka palvelin hyväksyy / hylkää.
Siirron kelvollisuus riippuu yleensä asioista, jotka asiakaskin väistämättä tietää (esim. omat kortit ja pöydällä näkyvät kortit), joten turhaa liikennettä voi hyvin välttää sillä, että asiakas ensin tarkistaa itse siirtonsa. Tietenkin tarkistus on syytä tehdä lisäksi palvelimella, ja jos tuli laiton siirto, potkitaan asiakas pihalle.
Tarkistus tosiaan tapahtuu molemmissa päissä, mutta asiakasta ei kumminkaan potkita pellolle. Projekti on nyt vähän tauolla, kun koulut alkoivat.
Aihe on jo aika vanha, joten et voi enää vastata siihen.