Skip to content

Technisch Ontwerpen

Mitchel Klijn edited this page May 23, 2025 · 27 revisions

Multiplayer

Inleiding

Onze opdrachtgever wilde graag een Zeeslag game hebben dat je met andere mensen kan spelen op een apart apparaat. Daarvoor hebben we multiplayer functionaliteit nodig in Unity. Gelukkig zijn er veel plugins om uit te kiezen, maar we moesten wel uitzoeken welke we het beste konden gebruiken en hoe we dat moesten doen.

Onderzoek

We hebben eerst opgezocht welke plugins Unity ondersteund. Ook hebben we gefilterd op onze specifieke benodigdheden, zoals een server-client systeem i.p.v. distributed authority, cheating prevention functies en optimalisatie voor hoeveelheid clients.

Mirror Networking

In eerste instantie kwamen we uit op Mirror Networking. Deze plugin was namelijk al bekend bij ons, dus we gingen meteen kijken of deze ook bij ons project past. Helaas was het maken van een lobby systeem zoals we in gedachten hadden niet mogelijk en zou het joinen van specifieke mensen niet makkelijk worden.

Fish-Net

Daarna vonden we Fish-Net. Deze plugin heeft veel goede reviews en zag er op het eerste gezicht veel belovend uit. Echter kwamen we er snel achter dat de manier die ze gebruiken best ingewikkeld is en de documentatie niet heel duidelijk was voor ons.

Netcode for GameObjects

Uiteindelijk zijn we gekomen op Netcode for GameObjects. Dit is de standaard aangeraden multiplayer plugin in de Unity Multiplayer Center. Dit betekend dat er veel meer duidelijke documentatie te vinden was en het simpeler gebruikt kan worden. Dit is de plugin die we gebruiken om GameObjects te netwerken.

Multiplayer Widgets, Services, Play Mode & Tools

Er waren ook nog extra plugins die Unity aanraadde om het testen, debuggen en het maken van lobby's makkelijker te maken.

Widgets

Multiplayer Widgets maakt het ons makkelijker om sessies aan te maken met UI en om een lijst te krijgen met gevonden sessies op andere apparaten.

Services

Multiplayer Widgets maakt gebruik van Multiplayer Services om ook daadwerkelijk sessies op de achtergrond te maken en te beheren.

Play Mode

Multiplayer Play Mode geeft in de editor de mogelijkheid om virtuele spelers op te starten, zodat we multiplayer kunnen simuleren op één apparaat en het netwerken kunnen testen.

Tools

Multiplayer Tools geeft ons functies om het netwerk te profilen en te debuggen. Het laat ons ook zien wat de status van het netwerk is.

NetworkManager

In de game hierarchy komt een leeg GameObject te staan met een NetworkManager component waarin alle instellingen voor het netwerk aangepast kunnen worden. Dit is een built-in script van de plugin. Dit object zorgt uiteindelijk voor de connecties tussen clients.

NetworkManager

DataHandler

Er komt ook een DataHandler, wat een leeg GameObject is met een NetworkObject component van de plugin wat er voor zorgt dat het genetwerkt wordt voor alle clients. Ook komt er een NetworkBehaviour script op te staan waarin alle variabelen met spelerdata bijgehouden worden over het netwerk.

NetworkVariables

De variabelen in dat script zijn een speciaal type genaamt NetworkVariable. Dit type komt van de plugin en heeft de extra functionaliteit om data te synchroniseren over alle clients en de server. Ook wanneer er een nieuwe speler joint, worden alle variabelen in dat script gesynchroniseerd op de lokale client.

Andere scripts kunnen functies aanroepen bij een verandering als ze geabonneerd zijn op de OnValueChanged van de variabelen. Zo kan data gemakkelijk gestuurd worden naar alle clients en kan de server alles beheren.


Speler raster

Inleiding

Voor het neerzetten van je eigen schepen en het doorgeven van de positie van je schepen aan de server hebben we een raster nodig die dit regelt. Het moet een 10x10 veld zijn waar je schepen met verschillende maten op kan zetten en kijken of ze op het raster gezet mogen worden, als een soort Tetris veld.

Onderzoek

Ik wou graag uitzoeken hoe ik het beste de verschillende boten hun "cells" kan rond draaien op het raster. Mijn eerste idee was om per boot een eigen n * n matrix te geven en die te draaien in de berekeningen:

Met de klok mee:

$$f(M) \Rightarrow M, \quad \text{where} \quad M_{ij} \mapsto M_{(n- j + 1), i}, \quad \forall i, j \in \{1, \dots, n\}$$

En tegen de klok in:

$$f(M) \Rightarrow M, \quad \text{where} \quad M_{ij} \mapsto M_{j, (n - i + 1)}, \quad \forall i, j \in \{1, \dots, n\}$$

Dit zou er in code ongeveer zo uit zien:

Function ClockWise(matrix):
   For i = 0 to size
       For j = 0 to size
          rotation[j, size - 1 - i] = matrix[i,j]
   Return rotation

Function CounterClockWise(matrix):
   For i = 0 to size
       For j = 0 to size
          rotation[size - 1 - j, i] = matrix[i,j]
   Return rotation

Het probleem met deze oplossing is dat elke vorm een vierkante matrix moet zijn om te kunnen draaien. Dit zorgt voor teveel ongebruikte vakjes in de berekening vooral voor de grotere schepen waar we uiteindelijk maar ⅕ van de matrix gebruiken.

Oplossing

De oplossing waar ik uiteindelijk op ben gekomen gebruikt in plaats van matrixen een lijst met offsets en een middelpunt. Deze posities noem ik cells en de lijst de shape. om de gehele shape te draaien gebruik ik deze berekeningen:

Met de klok mee:

$$f(S): S_i = (x_i, y_i) \mapsto S'_i = (y_i, -x_i), \quad \forall i \in \{1, \dots, n\}$$

Tegen de klok in:

$$f(S): S_i = (x_i, y_i) \mapsto S'_i = (-y_i, x_i), \quad \forall i \in \{1, \dots, n\}$$

Dit zou er dan in code ongeveer zo uit zien:

Function ClockWise(shape):
   For i = 0 to size
       shape[i] = (y, -x)

Function CounterClockWise(shape):
   For i = 0 to size
       shape[i] = (-y, x)

Deze manier van rotatie is veel meer data efficiënt voor wat wij proberen te bereiken en maakt het ook makkelijker voor het checken van de raster wanneer je een boot plaatst. Omdat we vanaf het middelpunt elke offset toevoegen en dan controleren of het past.

GridHandler

De GridHandler class is een MonoBehaviour class, dit script regelt welke cellen van de raster een boot bevatten en berekend of boten op een "legale" plek worden gezet.

GridVisualizer

De GridVisualizer class is een MonoBehaviour class, dit script visualiseert wat er op de raster gebeurt zoals de cellen aangeven waar een boot geplaatst kan worden en of een van jouw boten is aangevallen/gezonken.

GridShape

De GridShape struct is het object wat de boot zijn grootte, vorm en rotatie definieert.

GridCell

De GridCell struct is het object dat de GridHandler gebruikt voor het berekenen van alles op het raster.

---
title: Spelerraster Systeem
---
classDiagram
    class MonoBehaviour
    MonoBehaviour <|-- GridHandler
    MonoBehaviour <|-- GridVisualizer
    GridHandler <--> GridVisualizer
    class GridHandler {
        +GridCell[,] grid
        +IsValid(GridCell)
    }
    class GridVisualizer {
        +GridShape currentShape
        +OnAttack()
    }
    class GridCell {
        +bool hasBoat
        +Vector2Int position
    }
    class GridShape {
        +Vector2Int[] offsets
        +GridCell GetCenter()
        +RotateClockWise()
        +RotateCounterClockWise()
    }
Loading

Raketaanval

Voor het visualiseren van de raketaanval willen we een raket een animatie over een boog laten volgen. Dit kunnen we doen via door de positie op een parabool of op Unity's AnimationCurve te berekenen.

Parabool

Met een parabool kunnen we een mooie symmetrische boog maken die de raket kan volgen de formule hiervoor is:

$$f(x) = a(x)^2+bx+c$$

Een probleem met deze oplossing is dat je niet kan weten hoe de parabool eruit moet zien tussen 2 punten (er zijn oneindig mogelijkheden), en dus door moet geven (of berekenen) waar het middelpunt (de parabool zijn vertex) zit. Dit moet dan voor elke cel gebeuren wat het best lastig/zwaar maakt om het schaalbaar te houden.

AnimationCurve

Unity's AnimationCurve is een object waar je meerdere punten in kan aangeven om daar uiteindelijk een grafiek op een tijdlijn van te krijgen. Dit betekent dat we heel simpel verschillende vormen kunnen geven aan de boog van de raket. Het probleem met deze oplossing is dat er om de boog goed te laten zien de 2 punten op dezelfde hoogte moeten zitten.

3D Bogen

Voor beide oplossingen wordt een 2D boog gebruikt, maar omdat de cellen in 3D zijn moet er een manier zijn om een 2D boog naar de 3D punten te mappen. We kunnen de normal tussen het start en eindpunt bereken.

$$\hat{\mathbf{N}} = \frac{\mathbf{B} - \mathbf{A}}{\|\mathbf{B} - \mathbf{A}\|}$$

De lengte van het startpunt x en z en eindpunt x en z.

$$\mathbf{M} = \|\mathbf{B}_{xz} - \mathbf{A}_{xz}\|$$

Hiermee kunnen we een nieuw punt op de boog berekenen en in 3D zetten. Waarbij P het punt op de boog is (x zit tussen 0 en M).

$$\mathbf{R} = \begin{pmatrix} \mathbf{P}_{x} \hat{\mathbf{N}}_{x} \\ \mathbf{P}_{y} \\ \mathbf{P}_{x} \hat{\mathbf{N}}_{z}\end{pmatrix} + \mathbf{A}$$

Oplossing

Wij gaan de oplossing met de AnimationCurve gebruiken zodat het makkelijker is alle variabelen aan te passen en te kunnen spelen met verschillende vormen.


FX Systeem

Inleiding

Voor het afspelen van verschillende particle en audio effecten hebben we een los FX systeem nodig waarin we kunnen aangeven wanneer een particle of audio effect afgespeeld moet worden.

Onderzoek

Er zijn 2 mogelijkheden die we kunnen gebruiken voor een simpel FX systeem in Unity. De eerste is Unity's ingebouwde animatie systeem gebruiken waarin we de verschillende effecten op een tijdlijn kunnen zetten wanneer ze afgespeeld moeten worden. Maar een beter idee lijkt ons om een eigen systeem te maken die loopt op zelf gemaakte timers zodat het makkelijker is om in de editor snel iets aan te passen.

Concept

We willen een simpel maar alsnog effectief systeem dat beide audio en particle effecten kan afspelen. We hebben een lijst van effecten waarvan de eerste in de lijst aan het begin van het effect wordt gestaan. Alle effecten daaronder geven aan of ze na of tegelijk met het vorige effect afgespeeld willen worden. Ook heeft elk effect een timer die regelt hoelang het duurt totdat het effect afgespeeld wordt.

---
title: FX Systeem
---
classDiagram
    class MonoBehaviour
    MonoBehaviour <|-- TimerHandler
    MonoBehaviour <|-- FXSystem
    class ETiming {
        <<Enumeration>>
        StartWithPrevious
        StartAfterPrevious
    }
    class EEffectType {
        <<Enumeration>>
        Particle
        Audio
    }
    
    class Effect {
        +EEffectType type
        +ETiming timing
        +Timer timer
        +ParticleSystem effect
        +AudioClip audio
    }
    class FXSystem {
        -List~Effect~ effects 
        +Play()
    }

    class TimerHandler {
        +TimerHandler instance$
        +UnityAction~float~ onTick
        -Start()
        -Update()
    }
    class Timer {
        <<Abstract>>
        #float initialTime
        #float Time
        +Start()
        +Stop()
        +Tick(float deltaTime)*
    }
    class CountdownTimer {
        +Tick(float deltaTime)
    }
Loading

Zeemijn

Inleiding

Voor de zeemijn mechanic hebben we een systeem nodig dat de speler zeemijnen kan plaatsen op het veld van de tegenspeler, en wanneer het spel begint de zeemijn ontploft als er een schip te dichtbij is in een 3x3 raster op het spel raster. Maar als de Schiedam (2 cel schip) zich in het 3x3 raster bevindt wordt de mijn ontmanteld.

Plaatsen

Tijdens de voorbereiding van het spel (pregame) moet de mijn knop zichtbaar zijn wanneer de mijn wordt geplaatst moet je een cel kunnen selecteren op het dashboard. Als er een keuze is gemaakt veranderd de mijn knop in een vuur knop om het plaatsen te bevestigen dit wordt dan met een RPC naar de andere speler gestuurd om als het spel begint de mijnen op de juiste plek te spawnen.

Aanvallen

Wanneer het spel begonnen is en de mijnen tevoorschijn zijn gekomen checken ze een 3x3 raster om zich heen voor een schip. Deze check begint op de plaats van de mijn en gaat dan vanaf links met de klok mee. Als er een schip op het midden van het raster staat (waar de mijn zit) dan ontploft die cel, anders wordt er uit alle cellen een willekeurige cel gekozen die wordt aangevallen. als er geen aanval was of als de mijn niet ontmanteld is blijft het mijn logo op het dashboard zodat die cel niet geselecteerd kan worden.

Ontmantelen

Voordat de mijn aanvalt wordt er gecontroleerd of de Schiedam in het 3x3 raster zit zo ja wordt de zeemijn ontmanteld en verdwijnt het mijn icoontje van het dashboard van de eigenaar van de mijn om aan te geven dat er geen aanval is gebeurd op die mijn.

timeline
   title Zeemijn tijdlijn
   Pregame : Mijnen en Schepen plaatsen
   Ready : Wachten tot alle spelers klaar zijn
   Game: Ontmanteling check
   : Aanval check midden
   : Aanval check buiten
   : Aanval
   : UI bijwerken 
Loading

Torpedo

Inleiding

Voor de torpedo mechanic moeten we op het dashboard een "torpedomodus" in kunnen gaan dat knoppen aan zet waarmee we torpedo's naar de andere speler kunnen afvuren. De afgevuurde torpedo moet dan net zoals de raket inspawnen en bewegen naar de aangegeven richting. De torpedo moet het eerst volgende schip raken of alles missen wanneer het geen schip tegenkomt.

Richting Kiezen

Wanneer de speler een richting aanklikt, wordt er een "cell ID" doorgegeven aan de SetTargetCell functie met een richting enumerator. Dat ID is hetzelfde als die van de normale cell aan die kant van het raster, maar dan plus 100. Op die manier kan de CellUnpacker checken of er een torpedo ID is aangegeven en weet dan aan welke kant de torpedo moet spawnen. De torpedo wordt dan gespawnt door de TorpedoHandler met een GridOffset dat de afstand tot het raster is waarvan de torpedo moet beginnen.

Aanvallen

Wanneer de torpedo in de wereld zit, TorpedoBehaviour zorgt ervoor dat de torpedo in de juiste richting beweegt. Met behulp van een Lerp, de torpedo kan zelf checken in welke cell die zich bevindt en kijkt dan of die cell bezet is door een schip. Zo ja, dan ontploft de torpedo en wordt Attack aangeroepen. Zo niet, dan gaat de torpedo door tot het einde van de Lerp en mist dan.

Clone this wiki locally