Master Server: A centralised server for accepting and distributing available game servers.
I undertook this project originally as part of a much larger and complete multiplayer architecture for the learning experience. My primary motivation to reinvigorate my knowledge and exceed the shortcomings of Archmage. Scope being as large as it was and time escaping, I reduced the scope to a solution of what I saw as a shortcoming of Unity3D itself. The shortcoming as I saw it lay within the pricing scheme for their multiplayer services. That is, this service charges for all the data sent through it, while necessitating all data for a multiplayer game to be sent through the same service. With Unity3D being, as I see it, an engine for indie developers trying to make it big, “big” would be limited to what is immediately fundable using these services. My solution to replace these services provides a master server with minimal other functionality in order to keep running costs low. Accompanying this, I have also developed a client to interface the master server as well as distribute the rich information about available game server necessary for players to make informed decisions. This project gave me a thorough understanding of Unity3D’s networking LLAPI and refined my technique for socket based servers.
Master Server Repository/// {summary}
/// Asynchronously connect to the given server.
/// {/summary}
/// {param name="address"}The address to connect to.{/param}
/// {param name="port"}The port to connect to.{/param}
/// {param name="finish"}(int) Connection ID on success 0 otherwise. (string) description of any error that occurred{/param}
private IEnumerator ConnectTo(string address, int port, Action{int, string} finish) {
byte error;
int conID = NetworkTransport.Connect(mSClinetSocket, System.Net.Dns.GetHostAddresses(address)[0].ToString(), port, 0, out error);
if ((NetworkError)error != NetworkError.Ok) {
finish(0, "Failed to allocate resources for connection.");
yield break;
}
bool connected = false;
bool disconnected = false;
bool receivedError = false;
OnConnectEvent TestConnected = (recConID) => { connected = (recConID == conID) ? true : connected; };
OnDisconnectEvent TestDisconnected = (recConID) => { disconnected = (recConID == conID) ? true : disconnected; };
OnReceiveErrorEvent TestError = (recConID, recError) => { receivedError = (recConID == conID) ? true : receivedError; };
OnDisconnect += TestDisconnected;
OnConnect += TestConnected;
OnReceiveError += TestError;
yield return new WaitUntil(() => { return connected || disconnected || receivedError; });
OnReceiveError -= TestError;
OnConnect -= TestConnected;
OnDisconnect -= TestDisconnected;
if (connected) {
finish(conID, "");
} else {
if (address == masterServerURL && port == masterServerPort) {
finish(0, "Connection to the master server could not be established.");
} else {
finish(0, "Connection to " + address + ":" + port + " could not be established.");
}
}
}
Source: https://bitbucket.org/gerardryan/unity3d-master-server-client I completed the BGIE majoring in Software Technologies and minoring in Advanced software Technologies at QUT in Three years and one semester. Though claiming to be primarily games related it was essentially a bachelor of IT with games electives and a games related capstone project. Despite receiving the lacklustre content of the general software engineering units anyway, I would not choose the bachelor of IT given another chance. The BGIE’s capstone project was the real draw to this bachelor and proved to be an excellent experience working and communicating with team members from completely different educational backgrounds to complete a product.
Year | Semester | Title | Mark | Grade | Credit Points |
---|---|---|---|---|---|
2016 | 1 | Microprocessors and Digital Systems | 100% | High Distinction | 12 |
2016 | 1 | High Performance and Parallel Computing | 93% | High Distinction | 12 |
2016 | 1 | Network and Systems Administration | 93% | High Distinction | 12 |
2016 | 1 | Modelling Information Systems | 88% | High Distinction | 12 |
2015 | 2 | Games Project | 83% | Distinction | 24 |
2015 | 2 | Modelling and Animation Techniques | 82% | Distinction | 12 |
2015 | 1 | Discrete Structures | 96% | High Distinction | 12 |
2015 | 1 | Game Project Design | 84% | Distinction | 12 |
2015 | 1 | Real Time Rendering Techniques | 80% | Distinction | 12 |
2015 | 1 | AI for Games | 96% | High Distinction | 12 |
2014 | 2 | Systems Programming | 79% | Distinction | 12 |
2014 | 2 | Agile Software Development | N/A | Distinction | 12 |
2014 | 2 | Mathematical Tools for Computing | 86% | High Distinction | 12 |
2014 | 1 | Computer Technology Fundamentals | 88% | High Distinction | 12 |
2014 | 1 | Software Development | 81% | Distinction | 12 |
2014 | 1 | Data Structures and Algorithms | 93% | High Distinction | 12 |
2013 | 2 | Introduction to Games Production | 93% | High Distinction | 12 |
2013 | 2 | Networks | 92% | High Distinction | 12 |
2013 | 2 | Programming | 93% | High Distinction | 12 |
2013 | 1 | Industry Insights | 77% | Distinction | 12 |
2013 | 1 | Building IT Systems | 95% | High Distinction | 12 |
2013 | 1 | Computer Game Studies | 76% | Distinction | 12 |
2013 | 1 | Introducing Design | 77% | Distinction | 12 |
2015 Dean's List Award - Semester 1 - Science and Engineering
2016 Dean's List Award - Semester 1 - Science and Engineering
This project was about showing of what web-based technologies I am capable of utilizing as well as having a cool portfolio to show myself off with concrete examples. Notable tasks were repurposing this Material Framework to allow different navbar layouts for varying viewing devices, and editing/adding additional CSS like this groovy drop down box.
var DropdownList = {
initialised: false,
init: function () {
if (this.initialised) return;
this.onResize();
window.addEventListener("resize", this.onResize);
var eventName = this.transitionEndEventName();
var dropdownLists = document.getElementsByClassName("dropdownList");
for (var i = 0; i < dropdownLists.length; i++) {
var headers = dropdownLists[i].getElementsByClassName("header");
for (var j = 0; j < headers.length; j++) {
var body = headers[j].nextElementSibling;
body.addEventListener(eventName, this.onTransitionEnd);
if (j == 0) { // expand first element
this.onClick(headers[j])
}
}
}
this.initialised = true;
},
onResize: function () {
var dropdownLists = document.getElementsByClassName("dropdownList");
for (var i = 0; i < dropdownLists.length; i++) {
var headers = dropdownLists[i].getElementsByClassName("header");
for (var j = 0; j < headers.length; j++) {
var body = headers[j].nextElementSibling;
var lastHidden = body.hidden;
body.style.maxHeight = "none";
body.hidden = false;
body.style.maxHeight = getComputedStyle(body).height;
body.hidden = lastHidden;
}
}
},
onClick: function (ele) {
var body = ele.nextElementSibling
body.hidden = !body.hidden
var otherClasses = ele.lastElementChild.className.replace(/icon-[a-z/-]* /i, "");
if (body.hidden) {
ele.lastElementChild.className = "icon-arrow-drop-down " + otherClasses;
} else {
ele.lastElementChild.className = "icon-arrow-drop-up " + otherClasses;
}
},
onTransitionEnd: function (event) {
// now we have time to load the element fully set correct max-height
if (event.propertyName == "max-height" && !event.srcElement.hidden) {
event.srcElement.style.maxHeight = "none";
event.srcElement.style.maxHeight = getComputedStyle(event.srcElement).height;
}
},
Source: https://bitbucket.org/gerardryan/gerardryan.bitbucket.org I undertook this class as an elective in my final semester. My intentions were to concrete my programming foundations with knowledge of interfacing hardware at a low level and working in minimal memory environments such as microcontrollers. While accumulating 100% of the marks for this class I obtained a new perspective on bit manipulation and a better understanding of hardware and how processes interface it. With IoT becoming quite popular and more commonly used I believe that the understanding of serial communication protocols such as USART from microcontrollers will prove to be a very beneficial learning outcome. A notable personal achievement with the first assignment was developing an extensible and typical game loop with functioning delta time and virtual joysticks, though not required by specification of the first assignment.
int main(int argc, const char* argv[]) {
//auto_save_screen = true;
// screen and application setup
setup_screen();
override_screen_size(SCREEN_WIDTH, SCREEN_HEIGHT);
gameStartTime = get_current_time();
srand(SEED);
// setup timers
screenRefresh = create_timer(1000 / REFRESH_RATE);
platformSpawn = create_timer(PLATFORM_SPAWN_TIME);
resetJoystickX = create_timer(JOYSTICK_TIMEOUT);
resetJoystickY = create_timer(JOYSTICK_TIMEOUT);
bossSpawn = create_timer(rand() % BOSS_SPAWN_TIME_RANGE);
onStartup();
double loopStartTime;
while (!quit) {
loopStartTime = get_current_time();
onUpdate();
onDraw();
deltaTime = get_current_time() - loopStartTime;
}
onCleanup();
destroy_timer(screenRefresh);
destroy_timer(platformSpawn);
destroy_timer(resetJoystickX);
destroy_timer(resetJoystickY);
destroy_timer(bossSpawn);
cleanup_screen();
return EXIT_SUCCESS;
}
Source: https://bitbucket.org/gerardryan/cab202 This project was undertaken as an assignment for a parallel programming class, the objective was to optimize a selected piece of software and ideally having it running faster than the original sequential code. Unfortunately, I did not achieve a speed up due to the nature of GP-GPU computing and the data sizes of the selected software. However, I still achieved great marks due to achieving scalable parallelism (shown below) and undertaking a significantly difficult problem and coming out the other side alive with something to show. Though I must admit, only just. This project taught me not only the current extent of GP-GPU computing but also the limitations and bottlenecks that must be considered when optimizing with this technology. Another notable personal achievement with this assessment piece was implementing essentially AFR with regards to compute shaders. Specifically, as I had GPU devices available I allocated problems to each device alternatively reducing some of the memory overheads and reducing the overall runtime.
groupshared precise double2 cache[GROUP_DIM_X][4];
[numthreads(GROUP_DIM_X, 1, 1)]
void CSMain(uint3 Gid : SV_GroupID, uint3 GTid : SV_GroupThreadID, uint3 DTid : SV_DispatchThreadID) {
// calculate how many levels this kernal/workgroup can do
uint N = NMin;
uint NLast = 1 << (uint)(log2(xLen) - log2(dispatchX));
for (; N <= NLast; N <<= 1) {
uint subN = (xLen / N);
uint halfN = N / 2;
// Distribute threads evenly into the amount of N sections we have
uint groupNs = (uint)max((xLen / dispatchX) / N, 1);
uint threadsPerN = (uint)max(GROUP_DIM_X / groupNs, 1);
uint NsPerThread = (uint)max(groupNs / GROUP_DIM_X, 1);
uint threadWorkSize = (N * NsPerThread);
uint i = (uint)(DTid.x / threadsPerN) * threadWorkSize;
uint iLast = i + threadWorkSize;
for (; i < iLast; i += N) {
// Distribute threads evenly among the first half of N
uint threadWorkTimes = (uint)ceil(max(halfN / threadsPerN, 1));
for (uint w = 0; w < threadWorkTimes; w++) {
uint k = (uint)fmod(GTid.x, threadsPerN) + (w * threadsPerN);
if (k < halfN) {
uint evenIndex = i + k;
uint oddIndex = evenIndex + halfN;
cache[GTid.x][ODD] = Y[oddIndex];
cache[GTid.x][EVEN] = Y[evenIndex];
cache[GTid.x][TW_O] = twiddles[(k + halfN) * subN];
cache[GTid.x][TW_E] = twiddles[k * subN];
Y[evenIndex] = c_add(cache[GTid.x][EVEN], c_mul(cache[GTid.x][ODD], cache[GTid.x][TW_E]));
Y[oddIndex] = c_add(cache[GTid.x][EVEN], c_mul(cache[GTid.x][ODD], cache[GTid.x][TW_O]));
}
}
}
if (N != NLast) {
AllMemoryBarrierWithGroupSync();
}
}
}
This project was initially undertaken as a key data structure for implementing A* in a compute shader for an assignment on parallel computing. However Due to the scope of this idea and time constraints for the assignment I had to go with another idea. I consider this project notable as extensive research showed no fully functional array based AVL tree implementation was available to translate and adjust as required. I utilized this repository as a template to initially start but the tree rotations were incomplete for my needs and had to implement them myself from theory alone. In hindsight the constant and almost random additions to this data structure by an A* algorithm would almost render it useless for my intended project and would have been better off constructing my own memory allocation program within a compute shader.
// Where rootIndx is the highest point in the rotating tree
// not the root of the first Left rotation
void rotateRightLeft(int rootIndx) {
int newRootIndx = lChild(rChild(rootIndx));
// shift the root's left subtree down to the left
shiftDown(lChild(rootIndx), lChild(lChild(rootIndx)));
nodes[lChild(rootIndx)] = nodes[rootIndx];
// move the new roots left child to the roots left child's right child
shiftUp(lChild(newRootIndx), rChild(lChild(rootIndx)));
// move the new root node into the root node
nodes[rootIndx] = nodes[newRootIndx];
nodes[newRootIndx].key = NULL;
// shift up to where the new root was it's right child
shiftUp(rChild(newRootIndx), newRootIndx);
// adjust balances of nodes in their new positions
if (nodes[rootIndx].balance == 1) { // new root
nodes[lChild(rootIndx)].balance = 0; // old root
nodes[rChild(rootIndx)].balance = -1; // right from old root
} else if (nodes[rootIndx].balance == 0) {
nodes[lChild(rootIndx)].balance = 0;
nodes[rChild(rootIndx)].balance = 0;
} else {
nodes[lChild(rootIndx)].balance = 1;
nodes[rChild(rootIndx)].balance = 0;
}
nodes[rootIndx].balance = 0;
}
Source: https://bitbucket.org/gerardryan/array-based-avl-tree/ This was another personal project that was the inspired by and the initial foundation for long running project to make a remote controlled dart cannon. Other motivations for this project were to have an extensible and open source piece for my portfolio. This project was my first attempt at implementing a standard protocol (The WebSocket Protocol) and consequently my first experience of bit manipulation which proved to take up the most development time. As this protocol was only a minor part of this project it was only implemented to the minimum requirements for correct functionality, optional features such as security were left unimplemented. Though considerations were made and the framework shown here would allow these to be added easily. With regards to the overall functionality of having a WebSocket communicate with general input/output pins, this project also allowed me to refine my skills with multithreaded systems, specifically, a multithreaded socket server. After completing this project, I then modified it slightly to allow hardcoded security features for working with stepper motors. Source shown here and an image of the hardware running the server below.
int main(int argc, char* argv[]) {
// Connecting client information
struct sockaddr_in clientAddress;
socklen_t cliLen = sizeof(clientAddress);
//Signal interrupt handler
signal(SIGINT, interruptHandler);
// init globals as defaults
backlog = DEF_BACKLOG;
numThreads = DEF_NUM_THREADS;
port = DEF_PORT;
bufferlen = DEF_BUFFER_LENGTH;
ipLimit = SAME_IP_LIMIT;
ioTimeout.tv_sec = DEF_IO_TIMEOUT_SEC;
ioTimeout.tv_nsec = DEF_IO_TIMEOUT_NSEC;
pingTimeout.tv_sec = DEF_PING_TIMEOUT_SEC;
pingTimeout.tv_nsec = DEF_PING_TIMEOUT_NSEC;
// wiringPi
if (initWiringPi() == ERROR) {
error("ERROR on setting up wiringPi\n");
}
parseArgs(argc, argv);
listenFD = makeServerSocket(port);
pool = initThreadPool();
sockets = initSocketList();
connected = initSocketRefList();
printf("\nAdding Threads...");
for (int t = 0; t < numThreads; t++) {
addThread(&pool);
}
printf("\n%i threads added to pool", numThreads);
while (true) {
// Accept new connection
printf("\nReady and waiting for a new connection...\n");
int newSockFD = accept(listenFD, (struct sockaddr *) &clientAddress, &cliLen);
if (newSockFD == ERROR) {
error("ERROR on accept\n");
} else {
char* socketIP = inet_ntoa(clientAddress.sin_addr);
printf("\n%s:%d Connected\n", socketIP, (int)ntohs(clientAddress.sin_port));
if (!connectedTooManyTimes(socketIP, &connected, ipLimit)) {
addSocket(&sockets, newSockFD, socketIP, &connected);
} else {
close(newSockFD);
printf("Connection rejected, connected too many times\n");
}
}
}
}
Source: https://bitbucket.org/gerardryan/gpio-websocket-server Archmage, a mage themed, multiplayer third person shooter, with MOBA elements, was the capstone project for my Batchelor of Games and Interactive Entertainment. Archmage was completed within the academic year of 2015 and our team was awarded 6/7 for the end product. The team consisted of 7 persons for the majority of the term, their roles were a Programmer/Designer, a Designer, a character artist, an environmental artist, a texture and graphic artist, another programmer and myself, also a programmer. My roles primarily consisted with the network code, constructing production tools and some minor graphics programming, Though within these roles the Network code took up the vast majority of the time as it tended to be the most problematic.
This Unity3D project was undertaken as a piece of assessment for a real time rendering class. This class was first attempt at GPU programming and my introduction into their unforgiving nature. Despite being foretold as one of the harder classes QUT had to offer from the start, I was still able to achieve 28/30 for this project and 80% of the marks overall. Within the countless hours spent on this assessment alone I implemented common shader effects such as shadow mapping, Displacement Mapping (and the collision detection on unity’s side), Billboarding and various types light sources with normal mapping. Though this was my first experience with CG shaders I believe it was an excellent example of the quality of shaders I am capable of as well as a great foundation for GPU experimentation later to come.
// pixel shader
float4 AssignmentShaderPS(VSOutput input): COLOR {
// index into textures
float4 colour = tex2D(baseRGB, input.textureCoord);
float3 mapNormal = UnpackNormal(tex2D(normalMap, input.textureCoord)).rgb;
// Sun Light
float3 sunReflectVec = reflect(-input.tanSunLighDir, mapNormal);
float sunDiffuseComp = (max(dot(mapNormal, -input.tanSunLighDir), 0));
float sunSpecComp = (pow(max(dot(sunReflectVec, input.tanToCamera), 0), specPower));
// Moon Light
float3 moonReflectVec = reflect(-input.tanMoonLightDir, mapNormal);
float moonDiffuseComp = (max(dot(mapNormal, -input.tanMoonLightDir), 0));
float moonSpecComp = (pow(max(dot(moonReflectVec, input.tanToCamera), 0), specPower));
// Light 1 (Headlights)
float light1DiffuseComp = max(dot(mapNormal, input.tanToLight1), 0);
float3 light1ReflectVec = reflect(input.tanToLight1, mapNormal);
float light1SpecComp = pow(max(dot(light1ReflectVec, input.tanToCamera), 0), specPower);
float light1FallOff = 1;
if (light1Mode == SPOTLIGHT) {
light1FallOff = pow(max(dot(-input.tanToLight1, input.tanLight1Dir), 0), spot1FallOff);
}
// Light 2 (Torch in Window)
float light2DiffuseComp = max(dot(mapNormal, input.tanToLight2), 0);
float3 light2ReflectVec = reflect(input.tanToLight2, mapNormal);
float light2SpecComp = pow(max(dot(light2ReflectVec, input.tanToCamera), 0), specPower);
float light2FallOff = 1;
if (light2Mode == SPOTLIGHT) {
light2FallOff = pow(max(dot(-input.tanToLight2, input.tanLight2Dir), 0), spot2FallOff);
}
// Change to texture coordinates
ToTextureCoords(input.sunProjTex);
ToTextureCoords(input.light1ProjTex);
ToTextureCoords(input.light2ProjTex);
// Calculate scene Depth form the camera/light
float sunSceneDepth = SceneDepth(input.sunProjTex, SunNearClip, SunFarClip);
float light1SceneDepth = SceneDepth(input.light1ProjTex, Light1NearClip, Light1FarClip);
float light2SceneDepth = SceneDepth(input.light2ProjTex, Light2NearClip, Light2FarClip);
// sample the shadow map multiple times
float3x3 sunShadowSamples = DepthMapSamples(input.sunProjTex, sunSceneDepth, sunDepthMap, depthMapSampleScale * 4);
float3x3 light1ShadwoSamples = DepthMapSamples(input.light1ProjTex, light1SceneDepth, light1DepthMap, depthMapSampleScale);
float3x3 light2ShadowSamples = DepthMapSamples(input.light2ProjTex, light2SceneDepth, light2DepthMap, depthMapSampleScale);
// Calculate the shadow coefficient
float sunShadowCoeff = shadowCoeff(input.sunProjTex, sunShadowSamples);
float light1ShadowCoeff = shadowCoeff(input.light1ProjTex, light1ShadwoSamples);
float light2ShadowCoeff = shadowCoeff(input.light2ProjTex, light2ShadowSamples);
// calculate the three components
float3 specular = (((sunSpecComp * input.sunAbvHorizAmt) * sunShadowCoeff) * (sunLightSpecular * colour).rgb)
+ ((moonSpecComp * input.moonAbvHorizAmt) * (moonLightSpecular * colour).rgb)
+ (((light1SpecComp * light1FallOff) * light1ShadowCoeff) * (light1Specular * colour).rgb)
+ (((light2SpecComp * light2FallOff) * light2ShadowCoeff) * (light2Specular * colour).rgb);
float3 diffuse = (((sunDiffuseComp * input.sunAbvHorizAmt) * sunShadowCoeff) * (sunLightDiffuse * colour).rgb)
+ ((moonDiffuseComp * input.moonAbvHorizAmt) * (moonLightDiffuse * colour).rgb)
+ (((light1DiffuseComp * light1FallOff) * light1ShadowCoeff) * (light1Diffuse * colour).rgb)
+ (((light2DiffuseComp * light2FallOff) * light2ShadowCoeff) * (light2Diffuse * colour).rgb);
float3 ambient = input.ambientComp * (lightAmbient.rgb * colour.rgb);
// use alpha as transparency
float alpha = (alphaTransparency < 1) ? 1.0f : colour.a;
float4 pixelColour = float4(specular + diffuse + ambient, alpha);
// If pixel has an alpha of 0 discard
clip(pixelColour.a - 0.01f);
return pixelColour;
}
This Project was undertaken as a piece of assessment for an AI for Games class, this assessment required us to implement reactive agents using finite state machines. Using Unity3D I chose to implemented the agent receivers using C# delegates, this allowed for easily maintainable and extensible code as well as an ultimately simple implementation. This assessment piece also taught me the widely used A* path finding algorithm. My implementation of this algorithm made use of Unity3D’s ray casting classes to allow a dynamic pathfinding which allowed agents to avoid dynamically moving obstacles as well as working for changing environments. Though this implementation is processor intensive it was sufficiently suitable for the size of the level I was working with. Full marks were achieved for this piece of assessment.
void Start() {
// should contain all action methods in this script
actionArray = new Delegate[StateMachines.actionsCount] {this.Rest, this.Patrol, this.Hide,
this.Attack, this.FindBox, this.TakeBoxToPoint,
this.Follow, this.Aim, this.Fire};
// default/First action
currAction = this.Rest;
currActionIndex = (int)ActionArray.REST;
stateMachine = StateMachines.GetStateMachine(stateMachineName);
}
// Update is called once per frame
void Update() {
this.currAction();
Debug.Log(this.name + " is Doing " + this.currAction.Method.Name);
}
void ChangeAction(int triggerIndex) {
this.currActionIndex = this.stateMachine[currActionIndex, triggerIndex];
Debug.Log(this.name + " Recieved: " + (Triggers)triggerIndex);
if (this.currAction != actionArray[currActionIndex]) {
this.currAction = actionArray[currActionIndex];
Debug.Log(this.name + " - New Action " + (ActionArray)currActionIndex);
}
}
public void PostTrigger(Triggers t) {
ChangeAction((int)t);
}
This project was undertaken as a piece of assessment for a data structures and algorithms class. This piece of assessment required us to implement a weighted, undirected graph where vertices were locations on a map, with this we then had to then implement algorithms to find the minimum spanning tree and shortest paths such as Dijkstra’s single source shortest path and breadth first search. This piece of assessment gave me a thorough understanding of graphs and all their benefits and applications as well as a taste of how intriguing algorithms can be. Achieving 29/30 for this assessment also makes it an excellent example of the C++ skills I acquired.
/** \brief
* Uses Kruskal's algorithm to find the
* Minimum Spanning Tree (MST) for this
* Graph. Stores the edges of the MST in the
* adjacency list of each Vertex. Returns the
* cost of the minimum spanning tree.
* \return double - The cost of the minimum spanning tree
*/
double Graph::minimumSpanningTreeCost() {
double minCost = 0;
DisjointSet disjointSet = DisjointSet(numVertices);
unsigned int edgeCount = 0;
while (!edges.empty() && edgeCount < numVertices - 1) {
Edge edge = *edges.top();
edges.pop();
int sourceId = edge.getSource()->getId();
int destinationId = edge.getDestination()->getId();
if (! disjointSet.sameComponent(sourceId, destinationId)) {
edgeCount++;
disjointSet.join(sourceId, destinationId);
getVertex(destinationId)->addAdjacency(sourceId);
getVertex(sourceId)->addAdjacency(destinationId);
minCost += edge.getWeight();
}
}
return minCost;
}